From c7ad42cf272c2c9873b31fecb3303a72e2540f88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81sd=C3=ADs=20Erna=20Gu=C3=B0mundsd=C3=B3ttir?= Date: Mon, 7 Oct 2024 14:20:12 +0000 Subject: [PATCH 01/16] feat(service-portal): feature flag resolver for documents (#16285) * fix: def info and alert * feat: add feature flag to resolver * fix: move ff call to seperate function --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/screens/Dashboard/Dashboard.tsx | 10 +- .../documents/src/lib/document.module.ts | 3 +- .../documents/src/lib/documentV2.resolver.ts | 36 ++++-- .../documents/src/lib/documentV2.service.ts | 108 ++++++++++++++++++ .../lib/models/v2/documentContent.model.ts | 5 +- libs/feature-flags/src/lib/features.ts | 2 +- .../DocumentLine/DocumentLineV3.tsx | 4 +- .../OverviewDisplay/DesktopOverviewV3.tsx | 4 +- .../documents/src/hooks/useDocumentListV3.ts | 2 +- .../src/screens/Overview/OverviewV3.tsx | 4 +- 10 files changed, 154 insertions(+), 24 deletions(-) diff --git a/apps/service-portal/src/screens/Dashboard/Dashboard.tsx b/apps/service-portal/src/screens/Dashboard/Dashboard.tsx index bd5f5a9b7c2e..4dcec472ff69 100644 --- a/apps/service-portal/src/screens/Dashboard/Dashboard.tsx +++ b/apps/service-portal/src/screens/Dashboard/Dashboard.tsx @@ -17,7 +17,7 @@ import { DocumentsPaths, DocumentLine, DocumentLineV3, - useDocumentList, + useDocumentListV3, } from '@island.is/service-portal/documents' import { LinkResolver, @@ -41,9 +41,7 @@ import { useFeatureFlagClient } from '@island.is/react/feature-flags' export const Dashboard: FC> = () => { const { userInfo } = useAuth() - const { filteredDocuments, data, loading } = useDocumentList({ - defaultPageSize: 8, - }) + const { data: organizations } = useOrganizations() const { formatMessage } = useLocale() const { width } = useWindowSize() @@ -56,6 +54,10 @@ export const Dashboard: FC> = () => { // Versioning feature flag. Remove after feature is live. const [v3Enabled, setV3Enabled] = useState() + const { filteredDocuments, data, loading } = useDocumentListV3({ + defaultPageSize: 8, + }) + const featureFlagClient = useFeatureFlagClient() useEffect(() => { const isFlagEnabled = async () => { diff --git a/libs/api/domains/documents/src/lib/document.module.ts b/libs/api/domains/documents/src/lib/document.module.ts index ec76a2873ae2..59c7788956ca 100644 --- a/libs/api/domains/documents/src/lib/document.module.ts +++ b/libs/api/domains/documents/src/lib/document.module.ts @@ -6,9 +6,10 @@ import { DocumentServiceV2 } from './documentV2.service' import { DocumentResolverV1 } from './documentV1.resolver' import { DocumentResolverV2 } from './documentV2.resolver' import { DocumentService } from './documentV1.service' +import { FeatureFlagModule } from '@island.is/nest/feature-flags' @Module({ - imports: [DocumentsClientV2Module, DocumentsClientModule], + imports: [DocumentsClientV2Module, DocumentsClientModule, FeatureFlagModule], providers: [ DocumentResolverV2, DocumentResolverV1, diff --git a/libs/api/domains/documents/src/lib/documentV2.resolver.ts b/libs/api/domains/documents/src/lib/documentV2.resolver.ts index 3622993197f9..221a2e87225f 100644 --- a/libs/api/domains/documents/src/lib/documentV2.resolver.ts +++ b/libs/api/domains/documents/src/lib/documentV2.resolver.ts @@ -16,7 +16,11 @@ import { Document as DocumentV2, PaginatedDocuments, } from './models/v2/document.model' - +import { + FeatureFlagGuard, + FeatureFlagService, + Features, +} from '@island.is/nest/feature-flags' import { PostRequestPaperInput } from './dto/postRequestPaperInput' import { DocumentInput } from './models/v2/document.input' import { DocumentServiceV2 } from './documentV2.service' @@ -35,13 +39,14 @@ import { DocumentConfirmActions } from './models/v2/confirmActions.model' const LOG_CATEGORY = 'documents-resolver' -@UseGuards(IdsUserGuard, ScopesGuard) +@UseGuards(IdsUserGuard, ScopesGuard, FeatureFlagGuard) @Resolver(() => PaginatedDocuments) @Audit({ namespace: '@island.is/api/document-v2' }) export class DocumentResolverV2 { constructor( private documentServiceV2: DocumentServiceV2, private readonly auditService: AuditService, + private readonly featureFlagService: FeatureFlagService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -53,6 +58,7 @@ export class DocumentResolverV2 { locale: Locale = 'is', @CurrentUser() user: User, ): Promise { + const ffEnabled = await this.getFeatureFlag() try { return await this.auditService.auditPromise( { @@ -62,12 +68,14 @@ export class DocumentResolverV2 { resources: input.id, meta: { includeDocument: input.includeDocument }, }, - this.documentServiceV2.findDocumentById( - user.nationalId, - input.id, - locale, - input.includeDocument, - ), + ffEnabled + ? this.documentServiceV2.findDocumentByIdV3( + user.nationalId, + input.id, + locale, + input.includeDocument, + ) + : this.documentServiceV2.findDocumentById(user.nationalId, input.id), ) } catch (e) { this.logger.info('failed to get single document', { @@ -82,10 +90,13 @@ export class DocumentResolverV2 { @Scopes(DocumentsScope.main) @Query(() => PaginatedDocuments, { nullable: true }) @Audit() - documentsV2( + async documentsV2( @Args('input') input: DocumentsInput, @CurrentUser() user: User, ): Promise { + const ffEnabled = await this.getFeatureFlag() + if (ffEnabled) + return this.documentServiceV2.listDocumentsV3(user.nationalId, input) return this.documentServiceV2.listDocuments(user.nationalId, input) } @@ -206,4 +217,11 @@ export class DocumentResolverV2 { throw e } } + + private async getFeatureFlag(): Promise { + return await this.featureFlagService.getValue( + Features.isServicePortalDocumentsV3PageEnabled, + false, + ) + } } diff --git a/libs/api/domains/documents/src/lib/documentV2.service.ts b/libs/api/domains/documents/src/lib/documentV2.service.ts index aaf967fc8dbe..e22ced843676 100644 --- a/libs/api/domains/documents/src/lib/documentV2.service.ts +++ b/libs/api/domains/documents/src/lib/documentV2.service.ts @@ -36,6 +36,51 @@ export class DocumentServiceV2 { async findDocumentById( nationalId: string, documentId: string, + ): Promise { + const document = await this.documentService.getCustomersDocument( + nationalId, + documentId, + ) + + if (!document) { + return null // Null document logged in clients-documents-v2 + } + + let type: FileType + switch (document.fileType) { + case 'html': + type = FileType.HTML + break + case 'pdf': + type = FileType.PDF + break + case 'url': + type = FileType.URL + break + default: + type = FileType.UNKNOWN + } + + return { + ...document, + publicationDate: document.date, + id: documentId, + name: document.fileName, + downloadUrl: `${this.downloadServiceConfig.baseUrl}/download/v1/electronic-documents/${documentId}`, + sender: { + id: document.senderNationalId, + name: document.senderName, + }, + content: { + type, + value: document.content, + }, + } + } + + async findDocumentByIdV3( + nationalId: string, + documentId: string, locale?: string, includeDocument?: boolean, ): Promise { @@ -123,6 +168,69 @@ export class DocumentServiceV2 { nationalId, }) + if (typeof documents?.totalCount !== 'number') { + this.logger.warn('Document total count unavailable', { + category: LOG_CATEGORY, + totalCount: documents?.totalCount, + }) + } + + const documentData: Array = + documents?.documents + .map((d) => { + if (!d) { + return null + } + + return { + ...d, + id: d.id, + downloadUrl: `${this.downloadServiceConfig.baseUrl}/download/v1/electronic-documents/${d.id}`, + sender: { + name: d.senderName, + id: d.senderNationalId, + }, + } + }) + .filter(isDefined) ?? [] + + return { + data: documentData, + totalCount: documents?.totalCount ?? 0, + unreadCount: documents?.unreadCount, + pageInfo: { + hasNextPage: false, + }, + } + } + + async listDocumentsV3( + nationalId: string, + input: DocumentsInput, + ): Promise { + //If a delegated user is viewing the mailbox, do not return any health related data + //Category is now "1,2,3,...,n" + const { categoryIds, ...restOfInput } = input + let mutableCategoryIds = categoryIds ?? [] + + if (input.isLegalGuardian) { + if (!mutableCategoryIds.length) { + mutableCategoryIds = (await this.getCategories(nationalId, true)).map( + (c) => c.id, + ) + } else { + mutableCategoryIds = mutableCategoryIds.filter( + (c) => c === HEALTH_CATEGORY_ID, + ) + } + } + + const documents = await this.documentService.getDocumentList({ + ...restOfInput, + categoryId: mutableCategoryIds.join(), + nationalId, + }) + if (typeof documents?.totalCount !== 'number') { this.logger.warn('Document total count unavailable', { category: LOG_CATEGORY, diff --git a/libs/api/domains/documents/src/lib/models/v2/documentContent.model.ts b/libs/api/domains/documents/src/lib/models/v2/documentContent.model.ts index fac02463df0b..2fbf5c02dd09 100644 --- a/libs/api/domains/documents/src/lib/models/v2/documentContent.model.ts +++ b/libs/api/domains/documents/src/lib/models/v2/documentContent.model.ts @@ -14,8 +14,9 @@ export class DocumentContent { @Field(() => FileType) type!: FileType - @Field({ + @Field(() => String, { description: 'Either pdf base64 string, html markup string, or an url', + nullable: true, }) - value!: string + value?: string | null } diff --git a/libs/feature-flags/src/lib/features.ts b/libs/feature-flags/src/lib/features.ts index 0749442f1775..c3b1d9d48cb2 100644 --- a/libs/feature-flags/src/lib/features.ts +++ b/libs/feature-flags/src/lib/features.ts @@ -57,7 +57,7 @@ export enum Features { ServicePortalNotificationsEnabled = 'isServicePortalNotificationsPageEnabled', servicePortalLawAndOrderModuleEnabled = 'isServicePortalLawAndOrderModuleEnabled', servicePortalDocumentsActionsEnabled = 'isServicePortalDocumentsActionsEnabled', - + isServicePortalDocumentsV3PageEnabled = 'isServicePortalDocumentsV3PageEnabled', //Occupational License Health directorate fetch enabled occupationalLicensesHealthDirectorate = 'isHealthDirectorateOccupationalLicenseEnabled', diff --git a/libs/service-portal/documents/src/components/DocumentLine/DocumentLineV3.tsx b/libs/service-portal/documents/src/components/DocumentLine/DocumentLineV3.tsx index 394d1be3a6dd..bb3fc311b048 100644 --- a/libs/service-portal/documents/src/components/DocumentLine/DocumentLineV3.tsx +++ b/libs/service-portal/documents/src/components/DocumentLine/DocumentLineV3.tsx @@ -17,7 +17,7 @@ import { useNavigate, useParams, } from 'react-router-dom' -import { useDocumentList } from '../../hooks/useDocumentListV3' +import { useDocumentListV3 } from '../../hooks/useDocumentListV3' import { useIsChildFocusedorHovered } from '../../hooks/useIsChildFocused' import { useMailAction } from '../../hooks/useMailActionV2' import { DocumentsPaths } from '../../lib/paths' @@ -75,7 +75,7 @@ export const DocumentLineV3: FC = ({ bookmarkSuccess, } = useMailAction() - const { fetchObject, refetch } = useDocumentList() + const { fetchObject, refetch } = useDocumentListV3() const { setActiveDocument, diff --git a/libs/service-portal/documents/src/components/OverviewDisplay/DesktopOverviewV3.tsx b/libs/service-portal/documents/src/components/OverviewDisplay/DesktopOverviewV3.tsx index d6abdd655eb5..a618dead8479 100644 --- a/libs/service-portal/documents/src/components/OverviewDisplay/DesktopOverviewV3.tsx +++ b/libs/service-portal/documents/src/components/OverviewDisplay/DesktopOverviewV3.tsx @@ -9,7 +9,7 @@ import NoPDF from '../NoPDF/NoPDF' import { SERVICE_PORTAL_HEADER_HEIGHT_LG } from '@island.is/service-portal/constants' import { useDocumentContext } from '../../screens/Overview/DocumentContext' import * as styles from './OverviewDisplay.css' -import { useDocumentList } from '../../hooks/useDocumentListV3' +import { useDocumentListV3 } from '../../hooks/useDocumentListV3' interface Props { activeBookmark: boolean @@ -25,7 +25,7 @@ export const DesktopOverview: FC = ({ useNamespaces('sp.documents') const { formatMessage } = useLocale() const { activeDocument } = useDocumentContext() - const { activeArchive } = useDocumentList() + const { activeArchive } = useDocumentListV3() if (loading) { return ( diff --git a/libs/service-portal/documents/src/hooks/useDocumentListV3.ts b/libs/service-portal/documents/src/hooks/useDocumentListV3.ts index 6baee903493a..0ffe4e76bf20 100644 --- a/libs/service-portal/documents/src/hooks/useDocumentListV3.ts +++ b/libs/service-portal/documents/src/hooks/useDocumentListV3.ts @@ -9,7 +9,7 @@ export const pageSize = 10 type UseDocumentListProps = { defaultPageSize?: number } -export const useDocumentList = (props?: UseDocumentListProps) => { +export const useDocumentListV3 = (props?: UseDocumentListProps) => { const { filterValue, page, diff --git a/libs/service-portal/documents/src/screens/Overview/OverviewV3.tsx b/libs/service-portal/documents/src/screens/Overview/OverviewV3.tsx index 806ff02558f9..427be66fccdc 100644 --- a/libs/service-portal/documents/src/screens/Overview/OverviewV3.tsx +++ b/libs/service-portal/documents/src/screens/Overview/OverviewV3.tsx @@ -21,7 +21,7 @@ import DocumentLine from '../../components/DocumentLine/DocumentLineV3' import { FavAndStashV3 } from '../../components/FavAndStash/FavAndStashV3' import DocumentDisplay from '../../components/OverviewDisplay/OverviewDocumentDisplayV3' import { useDocumentFilters } from '../../hooks/useDocumentFilters' -import { pageSize, useDocumentList } from '../../hooks/useDocumentListV3' +import { pageSize, useDocumentListV3 } from '../../hooks/useDocumentListV3' import { useKeyDown } from '../../hooks/useKeyDown' import { useMailAction } from '../../hooks/useMailActionV2' import { DocumentsPaths } from '../../lib/paths' @@ -58,7 +58,7 @@ export const ServicePortalDocumentsV3 = () => { totalPages, filteredDocuments, totalCount, - } = useDocumentList() + } = useDocumentListV3() const { handlePageChange, handleSearchChange } = useDocumentFilters() From eeabe7f6e3f40f01db53570531b140bdc5268108 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Eorkell=20M=C3=A1ni=20=C3=9Eorkelsson?= Date: Mon, 7 Oct 2024 15:56:04 +0000 Subject: [PATCH 02/16] fix(vehicles-bulk-mileage): Fixes after testing review (#16295) * fix: testing fixes v1 * fix: testing comments v2 * fix: better message * fix: function name * fix: duplicate loading --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../service-portal/assets/src/lib/messages.ts | 23 +++- .../VehicleBulkMileage/VehicleBulkMileage.tsx | 6 +- .../VehicleBulkMileageRow.tsx | 115 +++++++++++------- .../VehicleBulkMileageJobDetail.tsx | 51 ++++++-- .../src/components/TableGrid/TableGrid.tsx | 96 ++++++++------- libs/service-portal/core/src/lib/messages.ts | 4 + 6 files changed, 197 insertions(+), 98 deletions(-) diff --git a/libs/service-portal/assets/src/lib/messages.ts b/libs/service-portal/assets/src/lib/messages.ts index daa67aff96c3..eb688c5e5955 100644 --- a/libs/service-portal/assets/src/lib/messages.ts +++ b/libs/service-portal/assets/src/lib/messages.ts @@ -198,6 +198,10 @@ export const vehicleMessage = defineMessages({ id: 'sp.vehicles:vehicle-mileage-intro', defaultMessage: `Sýnir kílómetrastöðu fyrir hvert ár. Athugið að einungis er hægt að skrá einu sinni fyrir hvert tímabil, sjá nánar um það hér.`, }, + vehicleBulkMileageIntro: { + id: 'sp.vehicles:vehicle-bulk-mileage-intro', + defaultMessage: `Yfirlit yfir skráða kílómetrastöðu. Að minnsta kosti 30 dagar verða að líða á milli skráningar kílómetrastöðu, sjá nánar um það hér.`, + }, historyIntro: { id: 'sp.vehicles:vehicles-history-intro', defaultMessage: `Hér má nálgast upplýsingar um þinn ökutækjaferil úr ökutækjaskrá Samgöngustofu.`, @@ -912,6 +916,10 @@ export const vehicleMessage = defineMessages({ id: 'sp.vehicles:bulk-post-mileage', defaultMessage: 'Magnskrá kílómetrastöðu', }, + bulkPostMileageWithFile: { + id: 'sp.vehicles:bulk-post-mileage-with-file', + defaultMessage: 'Magnskrá með skjali', + }, jobOverview: { id: 'sp.vehicles:job-overview', defaultMessage: 'Yfirlit skráninga', @@ -1018,21 +1026,30 @@ export const vehicleMessage = defineMessages({ }, fileUploadAcceptedTypes: { id: 'sp.vehicles:file-upload-accepted-types', - defaultMessage: 'Tekið er við skjölum með endingu; .csv, .xls', + defaultMessage: 'Tekið er við skjölum með endingu; .csv', }, dataAboutJob: { id: 'sp.vehicles:data-about-job', - defaultMessage: 'Hér finnur þú upplýsingar um skráningu', + defaultMessage: + 'Hér finnur þú upplýsingar um skráningu. Að vinna úr magnskráningarskjali getur tekið þónokkrar mínútur. ', }, refreshDataAboutJob: { id: 'sp.vehicles:refresh-data-about-job', defaultMessage: - 'Til að sækja nýjustu stöðu er hægt að smella á "Uppfæra stöðu"', + 'Til að sækja nýjustu stöðu á skráningarkeyrslunni er hægt að smella á "Uppfæra stöðu"', }, refreshJob: { id: 'sp.vehicles:refresh-job', defaultMessage: 'Uppfæra stöðu', }, + mileagePostSuccess: { + id: 'sp.vehicles:mileage-post-success', + defaultMessage: 'Kílómetraskráning tókst', + }, + mileagePutSuccess: { + id: 'sp.vehicles:mileage-put-success', + defaultMessage: 'Uppfærsla á kílómetraskráningu tókst', + }, mileageHistoryFetchFailed: { id: 'sp.vehicles:mileage-history-fetch-failed', defaultMessage: 'Eitthvað fór úrskeiðis við að sækja fyrri skráningar', diff --git a/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.tsx b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.tsx index c22e2ae8201b..635218334b18 100644 --- a/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.tsx +++ b/libs/service-portal/assets/src/screens/VehicleBulkMileage/VehicleBulkMileage.tsx @@ -27,13 +27,12 @@ const VehicleBulkMileage = () => { const [vehicles, setVehicles] = useState>([]) const [page, setPage] = useState(1) const [totalPages, setTotalPages] = useState(1) - const [pageSize, setPageSize] = useState(10) - const { data, loading, error, fetchMore } = useVehiclesListQuery({ + const { data, loading, error } = useVehiclesListQuery({ variables: { input: { page, - pageSize, + pageSize: 10, }, }, }) @@ -111,6 +110,7 @@ const VehicleBulkMileage = () => { totalPages={totalPages} renderLink={(page, className, children) => ( @@ -118,6 +143,8 @@ const VehicleBulkMileageJobDetail = () => { { size="default" variant="utility" onClick={handleFileDownload} + disabled={!displayRegistrationData} > {formatMessage(vehicleMessage.downloadErrors)} @@ -201,7 +229,7 @@ const VehicleBulkMileageJobDetail = () => { - {!!registrations?.requests.length && + {displayRegistrationData && registrations?.requests.map((j) => ( @@ -223,10 +251,13 @@ const VehicleBulkMileageJobDetail = () => { ))} - {(!registrations || registrationLoading) && ( + {!displayRegistrationData && ( )} diff --git a/libs/service-portal/core/src/components/TableGrid/TableGrid.tsx b/libs/service-portal/core/src/components/TableGrid/TableGrid.tsx index 5bb99cdde91c..11f2be0c14fe 100644 --- a/libs/service-portal/core/src/components/TableGrid/TableGrid.tsx +++ b/libs/service-portal/core/src/components/TableGrid/TableGrid.tsx @@ -1,6 +1,14 @@ import React, { FC } from 'react' -import { Text, Table as T, Column, Columns } from '@island.is/island-ui/core' +import { + Text, + Table as T, + Column, + Columns, + SkeletonLoader, +} from '@island.is/island-ui/core' import { tableStyles } from '../../utils/utils' +import { EmptyTable } from '../EmptyTable/EmptyTable' +import { MessageDescriptor } from 'react-intl' interface TableItem { title: string @@ -13,6 +21,8 @@ interface Props { title?: string subtitle?: string mt?: boolean + loading?: boolean + emptyMessage?: MessageDescriptor } export const TableGrid: FC> = ({ @@ -20,6 +30,8 @@ export const TableGrid: FC> = ({ title, subtitle, mt, + loading, + emptyMessage, }) => { return ( @@ -36,46 +48,48 @@ export const TableGrid: FC> = ({ - {dataArray.map((row, ii) => ( - - {row.map( - (rowitem, iii) => - rowitem && ( - - - - - {rowitem.title} - - - - - {rowitem.value} - - - - - ), - )} - - ))} + {loading && } + {!loading && + dataArray.map((row, ii) => ( + + {row.map( + (rowitem, iii) => + rowitem && ( + + + + + {rowitem.title} + + + + + {rowitem.value} + + + + + ), + )} + + ))} ) diff --git a/libs/service-portal/core/src/lib/messages.ts b/libs/service-portal/core/src/lib/messages.ts index fc72b9ef77cc..7c459b2c841b 100644 --- a/libs/service-portal/core/src/lib/messages.ts +++ b/libs/service-portal/core/src/lib/messages.ts @@ -5,6 +5,10 @@ export const m = defineMessages({ id: 'service.portal:reference', defaultMessage: 'Tilvísun', }, + goToPage: { + id: 'service.portal:go-to-page', + defaultMessage: 'Fara á síðu', + }, print: { id: 'service.portal:print', defaultMessage: 'Prenta', From 618f23ff88a5a11d7e36ba5a84bda8665761f721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Svanhildur=20Einarsd=C3=B3ttir?= <54863023+svanaeinars@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:25:10 +0000 Subject: [PATCH 03/16] feat(tests): New @island/testing/e2e library (#16287) * Add @swc-node/register and @swc/core * Add testing/e2e library * update project.json for testing/e2e * fix import for libTestingE2e --------- Co-authored-by: Kristofer Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/testing/e2e/.eslintrc.json | 18 ++++++++++++++++++ libs/testing/e2e/README.md | 7 +++++++ libs/testing/e2e/jest.config.ts | 11 +++++++++++ libs/testing/e2e/project.json | 19 +++++++++++++++++++ libs/testing/e2e/src/index.ts | 1 + .../e2e/src/lib/libs/testing/e2e.spec.ts | 7 +++++++ libs/testing/e2e/src/lib/libs/testing/e2e.ts | 3 +++ libs/testing/e2e/tsconfig.json | 16 ++++++++++++++++ libs/testing/e2e/tsconfig.lib.json | 11 +++++++++++ libs/testing/e2e/tsconfig.spec.json | 14 ++++++++++++++ tsconfig.base.json | 1 + 11 files changed, 108 insertions(+) create mode 100644 libs/testing/e2e/.eslintrc.json create mode 100644 libs/testing/e2e/README.md create mode 100644 libs/testing/e2e/jest.config.ts create mode 100644 libs/testing/e2e/project.json create mode 100644 libs/testing/e2e/src/index.ts create mode 100644 libs/testing/e2e/src/lib/libs/testing/e2e.spec.ts create mode 100644 libs/testing/e2e/src/lib/libs/testing/e2e.ts create mode 100644 libs/testing/e2e/tsconfig.json create mode 100644 libs/testing/e2e/tsconfig.lib.json create mode 100644 libs/testing/e2e/tsconfig.spec.json diff --git a/libs/testing/e2e/.eslintrc.json b/libs/testing/e2e/.eslintrc.json new file mode 100644 index 000000000000..3456be9b9036 --- /dev/null +++ b/libs/testing/e2e/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/testing/e2e/README.md b/libs/testing/e2e/README.md new file mode 100644 index 000000000000..f5b4b36b7919 --- /dev/null +++ b/libs/testing/e2e/README.md @@ -0,0 +1,7 @@ +# libs/testing/e2e + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test libs/testing/e2e` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/testing/e2e/jest.config.ts b/libs/testing/e2e/jest.config.ts new file mode 100644 index 000000000000..c825cd0ac83c --- /dev/null +++ b/libs/testing/e2e/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'libs/testing/e2e', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/testing/e2e', +} diff --git a/libs/testing/e2e/project.json b/libs/testing/e2e/project.json new file mode 100644 index 000000000000..c40725ab3210 --- /dev/null +++ b/libs/testing/e2e/project.json @@ -0,0 +1,19 @@ +{ + "name": "testing-e2e", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/testing/e2e/src", + "projectType": "library", + "tags": ["lib:react", "scope:react"], + "targets": { + "lint": { + "executor": "@nx/eslint:lint" + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/libs/testing/e2e"], + "options": { + "jestConfig": "libs/testing/e2e/jest.config.ts" + } + } + } +} diff --git a/libs/testing/e2e/src/index.ts b/libs/testing/e2e/src/index.ts new file mode 100644 index 000000000000..88dfe575adfc --- /dev/null +++ b/libs/testing/e2e/src/index.ts @@ -0,0 +1 @@ +export * from './lib/libs/testing/e2e' diff --git a/libs/testing/e2e/src/lib/libs/testing/e2e.spec.ts b/libs/testing/e2e/src/lib/libs/testing/e2e.spec.ts new file mode 100644 index 000000000000..64546b648ec3 --- /dev/null +++ b/libs/testing/e2e/src/lib/libs/testing/e2e.spec.ts @@ -0,0 +1,7 @@ +import { libsTestingE2e } from './e2e' + +describe('libsTestingE2e', () => { + it('should work', () => { + expect(libsTestingE2e()).toEqual('libs/testing/e2e') + }) +}) diff --git a/libs/testing/e2e/src/lib/libs/testing/e2e.ts b/libs/testing/e2e/src/lib/libs/testing/e2e.ts new file mode 100644 index 000000000000..ead5b58b4769 --- /dev/null +++ b/libs/testing/e2e/src/lib/libs/testing/e2e.ts @@ -0,0 +1,3 @@ +export function libsTestingE2e(): string { + return 'libs/testing/e2e' +} diff --git a/libs/testing/e2e/tsconfig.json b/libs/testing/e2e/tsconfig.json new file mode 100644 index 000000000000..25f7201d870e --- /dev/null +++ b/libs/testing/e2e/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs" + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/testing/e2e/tsconfig.lib.json b/libs/testing/e2e/tsconfig.lib.json new file mode 100644 index 000000000000..e583571eac87 --- /dev/null +++ b/libs/testing/e2e/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/libs/testing/e2e/tsconfig.spec.json b/libs/testing/e2e/tsconfig.spec.json new file mode 100644 index 000000000000..69a251f328ce --- /dev/null +++ b/libs/testing/e2e/tsconfig.spec.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 36a85729ce40..8b5da3e7139f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1112,6 +1112,7 @@ ], "@island.is/skilavottord/types": ["libs/skilavottord/types/src/index.ts"], "@island.is/testing/containers": ["libs/testing/containers/src/index.ts"], + "@island.is/testing/e2e": ["libs/testing/e2e/src/index.ts"], "@island.is/testing/fixtures": ["libs/testing/fixtures/src/index.ts"], "@island.is/testing/nest": ["libs/testing/nest/src/index.ts"], "@island.is/university-gateway": ["libs/university-gateway/src/index.ts"], From 8c77303cab486c40a74529f50652d6bf69ea14cc Mon Sep 17 00:00:00 2001 From: helgifr Date: Mon, 7 Oct 2024 16:54:57 +0000 Subject: [PATCH 04/16] feat(parental-leave): ApplicationRights (#15901) * feat(parental-leave): ApplicationRights Added applicationRights to parental-leave when sending application. Since we are using a new way of calculating periods * Fix days used by period calculation * Tests for new periods * rename function with proper camelCase * Refactor: Made duplicate code into a function * Make ApplicationRights nullable * refactor: function instead of duplicate code * remove console.log * error handling for period data * clientConfig nullable fix * Fixes for calculation of months. And using clamp to get correct value of daysLeft * Multiply amount of months by 30 for period calculation with month durations * Fix old calculation of endDate with months --------- Co-authored-by: hfhelgason Co-authored-by: veronikasif <54938148+veronikasif@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../templates/parental-leave/constants.ts | 92 ++ .../parental-leave.service.spec.ts | 250 ++--- .../parental-leave/parental-leave.service.ts | 890 ++++-------------- .../parental-leave/parental-leave.utils.ts | 3 + .../src/fields/Duration/Duration.tsx | 3 +- .../src/lib/directorateOfLabour.utils.ts | 4 + .../src/lib/parentalLeaveUtils.ts | 48 +- .../templates/parental-leave/src/types.ts | 1 + libs/clients/vmst/src/clientConfig.yaml | 4 + libs/clients/vmst/src/lib/utils.ts | 3 + 10 files changed, 449 insertions(+), 849 deletions(-) diff --git a/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/constants.ts b/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/constants.ts index 0ed762beba59..9c9e3a0c39b3 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/constants.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/constants.ts @@ -36,3 +36,95 @@ export const isRunningInProduction = process.env.NODE_ENV === 'production' export const APPLICATION_ATTACHMENT_BUCKET = 'APPLICATION_ATTACHMENT_BUCKET' export const SIX_MONTHS_IN_SECONDS_EXPIRES = 6 * 30 * 24 * 60 * 60 export const df = 'yyyy-MM-dd' + +export const rightsDescriptions: { [key: string]: string } = { + ALVEIKT: 'Alv.veikt barn eftir heimk.', + 'ANDL.MEÐG': 'Andlát hins foreldris á meðgöngu', + ANDLÁT: 'Andlát maka', + ANDVANA18: 'Fósturlát eftir 18 vikur', + ANDVANA22: 'Andvana fætt eftir 22 vikur', + 'DVAL.FJÖL': 'Dvalarstyrkur, fjölburafæðing', + DVALSTYRK: 'Dvalarstyrkur', + EITTFOR: 'Eitt foreldri', + 'F-ANDV22': 'Andvana fætt eftir 22 vikur - faðir', + FANGELSI: 'Fangelsisvist', + 'F-FL-FS': 'Fæðingarst. forsjárlauss föður', + 'F-FL-FSN': 'Fæðst. forsl.föður í námi', + 'F-FL-L-GR': 'Gr.rétt. fors.lauss föður laun', + 'F-FL-L-GRS': 'Gr.rétt. fors.lauss föður laun/sjálfst.', + 'F-FL-S-GR': 'Gr.rétt. fors.lauss föður Sjál', + 'F-FÓ-FS': 'Fæðingastyrkur fósturföðurs', + 'F-FÓ-FSN': 'Fæðingastyrkur fósturf. í námi', + 'F-FÓ-GR': 'Grunnréttur fósturföðurs', + 'F-FÓ-GR-SJ': 'Grunnréttur fósturföðurs launþ/sjálfst.', + 'F-FÓ-S-GR': 'Grunnr.fósturföðurs sjálfst.', + 'F-FS': 'Fæðingastyrkur föður', + 'F-FSN': 'Fæðingarstyrkur föður í námi', + 'F-L-GR': 'Grunnréttur föður launþ.', + 'F-L-GR-SJ': 'Grunnréttur föður launþ./sjálfst.', + 'FO-ANDV22': 'Andvana fætt eftir 22 vikur - foreldri', + 'FO-FL-FS': 'Fæðingarst. forsjárlauss foreldris', + 'FO-FL-FSN': 'Fæðst. forsl.foreldris í námi', + 'FO-FL-L-GR': 'Gr.rétt. fors.lauss foreldris laun', + 'FO-FL-L-GS': 'Gr.rétt. fors.lauss foreldris laun/sjálfst.', + 'FO-FL-S-GR': 'Gr.rétt. fors.lauss foreldris Sjál', + 'FO-FÓ-FS': 'Fæðingastyrkur fósturforeldris', + 'FO-FÓ-FSN': 'Fæðingastyrkur fósturforeldris í námi', + 'FO-FÓ-GR': 'Grunnréttur fósturforeldris', + 'FO-FÓ-GR-S': 'Grunnréttur fósturforeldris launþ/sjálfst.', + 'FO-FÓ-S-GR': 'Grunnr.fósturforeldris sjálfst.', + 'FO-FS': 'Fæðingastyrkur foreldris', + 'FO-FSN': 'Fæðingarstyrkur foreldris í námi', + 'FO-L-GR': 'Grunnréttur foreldris launþ.', + 'FO-L-GR-SJ': 'Grunnréttur foreldris launþ./sjálfst.', + 'FO-S-GR': 'Grunnréttur foreldris sjálfst.', + 'FO-Æ-FS': 'Fæðingarstyrkur foreldris ættleið.', + 'FO-Æ-FSN': 'Fæðingarst. foreldris í námi ættl.', + 'FO-Æ-L-GR': 'Grunnréttur foreldris ættleiðing', + 'FO-Æ-L-GRS': 'Grunnréttur foreldris ættleiðing launþ/sjálfst.', + 'FO-Æ-S-GR': 'Grunnr. foreldris ættl. sjálfst.', + 'FSAL-GR': 'Framsal grunnréttur', + 'F-S-GR': 'Grunnréttur föður sjálfst.', + 'F-Æ-FS': 'Fæðingarstyrkur föður ættleið.', + 'F-Æ-FSN': 'Fæðingarst. föður í námi ættl.', + 'F-Æ-L-GR': 'Grunnréttur föður ættleiðing', + 'F-Æ-L-GRSJ': 'Grunnréttur föður ættleiðing launþ/sjálfst.', + 'F-Æ-S-GR': 'Grunnr. föður ættl. sjálfst.', + 'M-ANDV22': 'Andvana fætt eftir 22 vikur - móðir', + 'M-FL-FS': 'Fæðingarst. forsjárl. móður', + 'M-FL-FSN': 'Fæðst. forsl. móður í námi', + 'M-FL-L-GR': 'Grunnr. forsjárl. móður launþ.', + 'M-FL-L-GRS': 'Grunnr. forsjárl. móður launþ./sjálfst.', + 'M-FL-S-GR': 'Grunnr. forsjárl. móður sjálfs', + 'M-FÓ-FS': 'Fæðingarstyrkur fósturmóður', + 'M-FÓ-FSN': 'Fæðingarst. fósturmóður í námi', + 'M-FÓ-L-GR': 'Grunnréttur fósturmóður', + 'M-FÓ-L-GRS': 'Grunnréttur fósturmóður launþ/sjálfst.', + 'M-FÓ-S-GR': 'Grunnréttur fósturmóður sjálfs', + 'M-FS': 'Fæðingastyrkur móður', + 'M-FSN': 'Fæðingarstyrkur móður í námi', + 'M-L-GR': 'Grunnréttur móður', + 'M-L-GR-SJ': 'Grunnréttur móður launaþ./sjálfst.', + 'M-S-GR': 'Grunnréttur móður sjálfst.', + 'M-Æ-FS': 'Fæðingarstyrkur móður ættleið.', + 'M-Æ-FSN': 'Fæðingarst. móður í námi ættl.', + 'M-Æ-L-GR': 'Grunnr.móður ættleiðing', + 'M-Æ-L-GRSJ': 'Grunnr.móður ættleiðing launþ./sjálfst.', + 'M-Æ-S-GR': 'Grunnr.móður ættleiðing', + 'NÁLG.BANN': 'Nálgunarbann', + 'ORLOF-FBF': 'Fjölburafæðing (orlof)', + 'ORLOF-FBVF': 'Fjölfóstur (orlof)', + 'ORLOF-FBÆ': 'Fjölættleiðing (orlof)', + ÓFEÐRAÐ: 'Ófeðrað barn', + RÉTTLAUST: 'Réttindaleysi hins foreldris', + 'SAM-GR': 'Sameiginlegur grunnréttur', + 'SJÚK/SLYS': 'Sjúkdómar og slys', + 'ST-FBF': 'Fjölburafæðing (styrkur)', + UMGENGNI: 'Framlenging v/takm eða engrar umg forsjárl. foreld', + VEIKBARN: 'Veikindi barns', + VEIKFÆÐING: 'Veikindi móður v/fæðingar', + VEIKMEÐG: 'Veikindi móður á meðgöngu', + VEIKTBARN7: 'Alvar.veikindi barns f. heimk.', + 'ÆTTL.STYRK': 'Ættleiðingarstyrkur', + 'ÖRYGGI-L': 'Leng. M vegna öryggisást.', +} diff --git a/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.service.spec.ts b/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.service.spec.ts index 92524c696bbd..ce4c304ef7c2 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.service.spec.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.service.spec.ts @@ -247,225 +247,157 @@ describe('ParentalLeaveService', () => { }) describe('createPeriodsDTO', () => { - it('should return 2 periods, one standard and one using the right period code', async () => { + it('should return 2 periods with "M-S-GR,ORLOF-FBF" rightsCodePeriod and ratio in days', async () => { const application = createApplication() - const res = await parentalLeaveService.createPeriodsDTO( - application, - nationalId, - ) + + set(application, 'answers.periods[1]', { + ratio: '80', + useLength: 'no', + startDate: '2025-03-12', + endDate: '2025-09-11', + }) + const periods = get(application.answers, 'periods') as object as Period[] + const rights = 'M-S-GR,ORLOF-FBF' + + const res = parentalLeaveService.createPeriodsDTO(periods, false, rights) expect(res).toEqual([ { from: '2021-05-17', - to: '2021-11-16', - ratio: '100', + to: '2022-01-01', + ratio: 'D225', approved: false, paid: false, - rightsCodePeriod: 'M-L-GR', + rightsCodePeriod: rights, }, { - from: '2021-11-17', - to: '2022-01-01', - ratio: 'D45', + from: '2025-03-12', + to: '2025-09-11', + ratio: 'D144', approved: false, paid: false, - rightsCodePeriod: apiConstants.rights.receivingRightsId, + rightsCodePeriod: rights, }, ]) }) + }) - it('should return 2 periods, one standard and one using single parent right code', async () => { + describe('createRightsDTO', () => { + it('should return 2 applicationRights, basic rights, single parent rights, and multiple Birth', async () => { const application = createApplication() - set(application, 'answers.otherParent', SINGLE) + set(application, 'answers.periods[1]', { + ratio: '100', + useLength: 'no', + startDate: '2025-03-12', + endDate: '2025-09-14', + }) + set(application, 'answers.multipleBirths.hasMultipleBirths', YES) + set(application, 'answers.multipleBirthsRequestDays', 79) + set(application, 'answers.multipleBirths.multipleBirths', 2) + set(application, 'answers.otherParentObj.chooseOtherParent', SINGLE) - const res = await parentalLeaveService.createPeriodsDTO( - application, - nationalId, - ) + const res = await parentalLeaveService.createRightsDTO(application) expect(res).toEqual([ { - from: '2021-05-17', - to: '2021-11-16', - ratio: '100', - approved: false, - paid: false, - rightsCodePeriod: 'M-L-GR', + days: '180', + daysLeft: '0', + months: '6.0', + rightsDescription: 'Grunnréttur móður', + rightsUnit: 'M-L-GR', }, { - from: '2021-11-17', - to: '2022-01-01', - ratio: 'D45', - approved: false, - paid: false, - rightsCodePeriod: apiConstants.rights.artificialInseminationRightsId, + days: '180', + daysLeft: '0', + months: '6.0', + rightsDescription: 'Eitt foreldri', + rightsUnit: 'EITTFOR', + }, + { + days: '90', + daysLeft: '42', + months: '3.0', + rightsDescription: 'Fjölburafæðing (orlof)', + rightsUnit: 'ORLOF-FBF', }, ]) }) - it('should return 2 periods, one standard and mark it as ActualDateOfBirth and one using the right period code', async () => { + it('should return 1 applicationRights, basic rights', async () => { const application = createApplication() - const firstPeriod = get(application.answers, 'periods[0]') as object - set( - firstPeriod, - 'firstPeriodStart', - StartDateOptions.ACTUAL_DATE_OF_BIRTH, - ) + set(application, 'answers.requestRights', {}) - const res = await parentalLeaveService.createPeriodsDTO( - application, - nationalId, - ) + const res = await parentalLeaveService.createRightsDTO(application) expect(res).toEqual([ { - from: 'date_of_birth', - to: '2021-11-16', - ratio: '100', - approved: false, - paid: false, - rightsCodePeriod: 'M-L-GR', - }, - { - from: '2021-11-17', - to: '2022-01-01', - ratio: 'D45', - approved: false, - paid: false, - rightsCodePeriod: apiConstants.rights.receivingRightsId, + days: '180', + daysLeft: '0', + months: '6.0', + rightsDescription: 'Grunnréttur móður', + rightsUnit: 'M-L-GR', }, ]) }) - it('should return 3 periods, one standard, one using single parent right code and one using multiple birth right code', async () => { + it('should return 2 applicationRights, basic rights, multiple Birth', async () => { const application = createApplication() - const firstPeriod = get(application.answers, 'periods[0]') as object - set(firstPeriod, 'endDate', '2022-07-09') - set(application.answers, 'otherParent', SINGLE) - set(application.answers, 'applicationType.option', PARENTAL_LEAVE) - set(application.answers, 'multipleBirths.hasMultipleBirths', YES) - set(application.answers, 'multipleBirths.multipleBirths', 2) - set(application.answers, 'multipleBirthsRequestDays', 90) + set(application, 'answers.multipleBirths.hasMultipleBirths', YES) + set(application, 'answers.multipleBirthsRequestDays', 79) + set(application, 'answers.multipleBirths.multipleBirths', 2) - const res = await parentalLeaveService.createPeriodsDTO( - application, - nationalId, - ) + const res = await parentalLeaveService.createRightsDTO(application) expect(res).toEqual([ { - from: '2021-05-17', - to: '2021-11-16', - ratio: '100', - approved: false, - paid: false, - rightsCodePeriod: 'M-L-GR', - }, - { - from: '2021-11-17', - to: '2022-05-16', - ratio: '100', - approved: false, - paid: false, - rightsCodePeriod: apiConstants.rights.artificialInseminationRightsId, + days: '180', + daysLeft: '0', + months: '6.0', + rightsDescription: 'Grunnréttur móður', + rightsUnit: 'M-L-GR', }, { - from: '2022-05-17', - to: '2022-07-09', - ratio: 'D53', - approved: false, - paid: false, - rightsCodePeriod: apiConstants.rights.multipleBirthsOrlofRightsId, + days: '79', + daysLeft: '34', + months: '2.6', + rightsDescription: 'Fjölburafæðing (orlof)', + rightsUnit: 'ORLOF-FBF', }, ]) }) - it('should return 3 periods, one standard, one using multiple birth right code and one using right period code', async () => { + it('should return 2 applicationRights, one basic rights and one for rights transfer', async () => { const application = createApplication() - const firstPeriod = get(application.answers, 'periods[0]') as object - set(firstPeriod, 'endDate', '2022-04-01') - set(application.answers, 'applicationType.option', PARENTAL_GRANT) - set(application.answers, 'multipleBirths.hasMultipleBirths', YES) - set(application.answers, 'multipleBirths.multipleBirths', 2) - set(application.answers, 'multipleBirthsRequestDays', 90) + set(application, 'answers.periods[0]', { + ratio: '100', + useLength: 'no', + startDate: '2025-03-12', + endDate: '2025-09-14', + }) - const res = await parentalLeaveService.createPeriodsDTO( - application, - nationalId, - ) + const res = await parentalLeaveService.createRightsDTO(application) expect(res).toEqual([ { - from: '2021-05-17', - to: '2021-11-16', - ratio: '100', - approved: false, - paid: false, - rightsCodePeriod: 'M-FS', + days: '180', + daysLeft: '0', + months: '6.0', + rightsDescription: 'Grunnréttur móður', + rightsUnit: 'M-L-GR', }, { - from: '2021-11-17', - to: '2022-02-16', - ratio: '100', - approved: false, - paid: false, - rightsCodePeriod: apiConstants.rights.multipleBirthsGrantRightsId, - }, - { - from: '2022-02-17', - to: '2022-04-01', - ratio: 'D45', - approved: false, - paid: false, - rightsCodePeriod: apiConstants.rights.receivingRightsId, + days: '45', + daysLeft: '42', + months: '1.5', + rightsDescription: 'Framsal grunnréttur', + rightsUnit: 'FSAL-GR', }, ]) }) - - it('should change period ratio to D when using .daysToUse', async () => { - const application = createApplication() - - const startDate = new Date(2022, 9, 10) - const endDate = new Date(2023, 0, 9) - const ratio = 0.59 - - const originalPeriods: Period[] = [ - { - startDate: startDate.toISOString().split('T')[0], - endDate: endDate.toISOString().split('T')[0], - ratio: `${ratio * 100}`, - firstPeriodStart: StartDateOptions.ESTIMATED_DATE_OF_BIRTH, - daysToUse: calculatePeriodLength( - startDate, - endDate, - ratio, - ).toString(), - }, - ] - - set(application.answers, 'periods', originalPeriods) - - const expectedPeriods: VmstPeriod[] = [ - { - approved: false, - from: originalPeriods[0].startDate, - to: originalPeriods[0].endDate, - paid: false, - ratio: `D${originalPeriods[0].daysToUse}`, - rightsCodePeriod: 'M-L-GR', - }, - ] - const res = await parentalLeaveService.createPeriodsDTO( - application, - nationalId, - ) - - expect(res).toEqual(expectedPeriods) - }) }) describe('sendApplication', () => { diff --git a/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.service.ts index 7e84e1d6cff8..709d9423793b 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.service.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.service.ts @@ -1,8 +1,5 @@ import { Inject, Injectable } from '@nestjs/common' import { S3 } from 'aws-sdk' -import addDays from 'date-fns/addDays' -import format from 'date-fns/format' -import cloneDeep from 'lodash/cloneDeep' import { getValueViaPath } from '@island.is/application/core' import { @@ -17,27 +14,39 @@ import { PERMANENT_FOSTER_CARE, ParentalRelations, SINGLE, - StartDateOptions, States, UnEmployedBenefitTypes, YES, + calculateDaysUsedByPeriods, calculatePeriodLength, getAdditionalSingleParentRightsInDays, getApplicationAnswers, getApplicationExternalData, getAvailablePersonalRightsInDays, - getAvailableRightsInDays, getMultipleBirthsDays, + getSelectedChild, + getTransferredDays, + getTransferredDaysInMonths, getUnApprovedEmployers, isParentWithoutBirthParent, + Period as AnswerPeriod, + getPersonalDays, + getPersonalDaysInMonths, + StartDateOptions, + getAdditionalSingleParentRightsInMonths, + clamp, + getMultipleBirthsDaysInMonths, } from '@island.is/application/templates/parental-leave' import { Application, ApplicationConfigurations, ApplicationTypes, - YesOrNo, } from '@island.is/application/types' -import type { Attachment, Period } from '@island.is/clients/vmst' +import type { + ApplicationRights, + Attachment, + Period, +} from '@island.is/clients/vmst' import { ApplicationInformationApi, ParentalLeaveApi, @@ -59,7 +68,7 @@ import { APPLICATION_ATTACHMENT_BUCKET, SIX_MONTHS_IN_SECONDS_EXPIRES, apiConstants, - df, + rightsDescriptions, } from './constants' import { generateApplicationApprovedByEmployerEmail, @@ -72,10 +81,9 @@ import { import { getType, checkIfPhoneNumberIsGSM, - getFromDate, - getRatio, getRightsCode, transformApplicationToParentalLeaveDTO, + getFromDate, } from './parental-leave.utils' import { generateAssignEmployerApplicationSms, @@ -83,6 +91,7 @@ import { generateEmployerRejectedApplicationSms, generateOtherParentRejectedApplicationSms, } from './smsGenerators' +import parseISO from 'date-fns/parseISO' interface VMSTError { type: string @@ -92,17 +101,6 @@ interface VMSTError { errors: Record } -interface AnswerPeriod { - startDate: string - endDate: string - ratio: string - firstPeriodStart?: string - useLength?: YesOrNo - daysToUse?: string - rawIndex?: number - rightCodePeriod?: string -} - @Injectable() export class ParentalLeaveService extends BaseTemplateApiService { s3 = new S3() @@ -678,89 +676,16 @@ export class ParentalLeaveService extends BaseTemplateApiService { return attachments } - async getCalculatedPeriod( - nationalRegistryId: string, - startDate: Date, - startDateString: string | undefined, - periodLength: number, - period: AnswerPeriod, - rightsCodePeriod: string, - ): Promise { - const periodObj = { - from: startDateString ?? format(startDate, df), - approved: false, - paid: false, - rightsCodePeriod: rightsCodePeriod, - } - if (period.ratio === '100') { - const isUsingNumberOfDays = period.daysToUse !== undefined - const getPeriodEndDate = - await this.parentalLeaveApi.parentalLeaveGetPeriodEndDate({ - nationalRegistryId, - startDate: startDate, - length: String(periodLength), - percentage: period.ratio, - }) - - if (getPeriodEndDate.periodEndDate === undefined) { - throw new Error( - `Could not calculate end date of period starting ${period.startDate} and using ${periodLength} days of rights`, - ) - } - - return { - ...periodObj, - to: format(getPeriodEndDate.periodEndDate, df), - ratio: getRatio( - period.ratio, - periodLength.toString(), - isUsingNumberOfDays, - ), - } - } else { - const isUsingNumberOfDays = true - - // Calculate endDate from periodLength, startDate and percentage ( period.ratio ) - const actualDaysFromPercentage = Math.floor( - periodLength / (Number(period.ratio) / 100), - ) - - const endDate = addDays(startDate, actualDaysFromPercentage) - - return { - ...periodObj, - to: format(endDate, df), - ratio: getRatio( - period.ratio, - periodLength.toString(), - isUsingNumberOfDays, - ), - } - } - } - - async createPeriodsDTO( + async createRightsDTO( application: Application, - nationalRegistryId: string, - ): Promise { - const { - periods: originalPeriods, - firstPeriodStart, - applicationType, - otherParent, - } = getApplicationAnswers(application.answers) + ): Promise { + const { applicationType, otherParent, isRequestingRights, periods } = + getApplicationAnswers(application.answers) const { applicationFundId } = getApplicationExternalData( application.externalData, ) - const answers = cloneDeep(originalPeriods).sort((a, b) => { - const dateA = new Date(a.startDate) - const dateB = new Date(b.startDate) - - return dateA.getTime() - dateB.getTime() - }) - let vmstRightCodePeriod = null if (applicationFundId) { try { @@ -794,602 +719,184 @@ export class ParentalLeaveService extends BaseTemplateApiService { } } - const periods: Period[] = [] - const maximumDaysToSpend = getAvailableRightsInDays(application) const maximumPersonalDaysToSpend = getAvailablePersonalRightsInDays(application) const maximumMultipleBirthsDaysToSpend = getMultipleBirthsDays(application) const maximumAdditionalSingleParentDaysToSpend = getAdditionalSingleParentRightsInDays(application) - const maximumDaysBeforeUsingTransferRights = - maximumPersonalDaysToSpend + maximumMultipleBirthsDaysToSpend - const maximumSingleParentDaysBeforeUsingMultipleBirthsRights = - maximumPersonalDaysToSpend + maximumAdditionalSingleParentDaysToSpend + const usedDays = calculateDaysUsedByPeriods(periods) + + const selectedChild = getSelectedChild( + application.answers, + application.externalData, + ) + if (!selectedChild) { + throw new Error('Missing selected child') + } + const transferredDays = getTransferredDays(application, selectedChild) + const personalDays = getPersonalDays(application) const mulitpleBirthsRights = applicationType === PARENTAL_LEAVE ? apiConstants.rights.multipleBirthsOrlofRightsId : apiConstants.rights.multipleBirthsGrantRightsId - const isActualDateOfBirth = - firstPeriodStart === StartDateOptions.ACTUAL_DATE_OF_BIRTH - let numberOfDaysAlreadySpent = 0 - const basicRightCodePeriod = - vmstRightCodePeriod ?? getRightsCode(application) - - for (const [index, period] of answers.entries()) { - const isFirstPeriod = index === 0 - const isUsingNumberOfDays = - period.daysToUse !== undefined && period.daysToUse !== '' - - // If a period doesn't have both startDate or endDate we skip it - if (!isFirstPeriod && (!period.startDate || !period.endDate)) { - continue - } - - const startDate = new Date(period.startDate) - const endDate = new Date(period.endDate) - const useLength = period.useLength || '' - - let periodLength = 0 - - if (isUsingNumberOfDays) { - periodLength = Number(period.daysToUse) - } else if (Number(period.ratio) < 100) { - /* - * We need to calculate periodLength when ratio is not 100% - * because there could be mis-calculate betweeen island.is and VMST - * for example: - * 8 months period with 75% - * island.is calculator returns: 180 days - * VMST returns: 184 days - */ - const fullLength = calculatePeriodLength(startDate, endDate) - periodLength = Math.round(fullLength * (Number(period.ratio) / 100)) - } else { - const getPeriodLength = - await this.parentalLeaveApi.parentalLeaveGetPeriodLength({ - nationalRegistryId, - startDate, - endDate, - percentage: period.ratio, - }) - - if (getPeriodLength.periodLength === undefined) { - throw new Error( - `Could not calculate length of period from ${period.startDate} to ${period.endDate}`, - ) - } - - periodLength = Number(getPeriodLength.periodLength ?? 0) - } - - const numberOfDaysSpentAfterPeriod = - numberOfDaysAlreadySpent + periodLength + const baseRight = vmstRightCodePeriod ?? getRightsCode(application) + const rights = [ + { + rightsUnit: baseRight, + days: String(personalDays), + rightsDescription: rightsDescriptions[baseRight], + months: String(getPersonalDaysInMonths(application)), + daysLeft: String(Math.max(0, personalDays - usedDays)), + }, + ] + + const addMultipleBirthsRights = ( + rightsArray: ApplicationRights[], + totalDays: number, + usedDays: number, + ) => { + rightsArray.push({ + rightsUnit: mulitpleBirthsRights, + days: String(maximumMultipleBirthsDaysToSpend), + rightsDescription: rightsDescriptions[mulitpleBirthsRights], + months: String(getMultipleBirthsDaysInMonths(application)), + daysLeft: String( + clamp( + totalDays + maximumMultipleBirthsDaysToSpend - usedDays, + 0, + maximumMultipleBirthsDaysToSpend, + ), + ), + }) + } - if (numberOfDaysSpentAfterPeriod > maximumDaysToSpend) { - throw new Error( - `Period from ${period.startDate} to ${period.endDate} will exceed rights (${numberOfDaysSpentAfterPeriod} > ${maximumDaysToSpend})`, + if (otherParent === SINGLE) { + rights.push({ + rightsUnit: apiConstants.rights.artificialInseminationRightsId, + days: String(maximumAdditionalSingleParentDaysToSpend), + rightsDescription: + rightsDescriptions[ + apiConstants.rights.artificialInseminationRightsId + ], + months: String(getAdditionalSingleParentRightsInMonths(application)), + daysLeft: String( + clamp( + maximumPersonalDaysToSpend + + maximumAdditionalSingleParentDaysToSpend - + usedDays, + 0, + maximumAdditionalSingleParentDaysToSpend, + ), + ), + }) + if (maximumMultipleBirthsDaysToSpend > 0) { + addMultipleBirthsRights( + rights, + personalDays + maximumAdditionalSingleParentDaysToSpend, + usedDays, ) } - - const isUsingAdditionalRights = - numberOfDaysAlreadySpent >= - maximumDaysToSpend - maximumAdditionalSingleParentDaysToSpend - const willSingleParentStartToUseAdditionalRightsWithPeriod = - numberOfDaysSpentAfterPeriod > - maximumDaysToSpend - maximumAdditionalSingleParentDaysToSpend - const isSingleParentUsingMultipleBirthsRights = - numberOfDaysAlreadySpent >= - maximumSingleParentDaysBeforeUsingMultipleBirthsRights - const isSingleParentUsingPersonalRights = - numberOfDaysAlreadySpent < maximumPersonalDaysToSpend - const willSingleParentStartUsingMultipleBirthsRight = - numberOfDaysSpentAfterPeriod > - maximumPersonalDaysToSpend + maximumAdditionalSingleParentDaysToSpend - - const isUsingMultipleBirthsRights = - numberOfDaysAlreadySpent >= maximumPersonalDaysToSpend - const willStartToUseMultipleBirthsRightsWithPeriod = - numberOfDaysSpentAfterPeriod > maximumPersonalDaysToSpend - const isUsingTransferredRights = - numberOfDaysAlreadySpent >= maximumDaysBeforeUsingTransferRights - const willStartToUseTransferredRightsWithPeriod = - numberOfDaysSpentAfterPeriod > maximumDaysBeforeUsingTransferRights - - /* - ** Priority rights: - ** 1. personal rights - ** 2. single parent rights - ** 3. common rights ( from multiple births) - ** 4. transfer rights ( from other parent) - We have to finish first one before go to next and so on - */ - if ( - !isUsingTransferredRights && - !willStartToUseTransferredRightsWithPeriod && - !isUsingMultipleBirthsRights && - !willStartToUseMultipleBirthsRightsWithPeriod && - !isUsingAdditionalRights && - !willSingleParentStartToUseAdditionalRightsWithPeriod - ) { - // We know its a normal period and it will not exceed personal rights - periods.push({ - from: getFromDate( - isFirstPeriod, - isActualDateOfBirth, - useLength, - period, - ), - to: period.endDate, - ratio: getRatio( - period.ratio, - periodLength.toString(), - period.ratio === '100' ? isUsingNumberOfDays : true, + } else { + if (maximumMultipleBirthsDaysToSpend > 0) { + addMultipleBirthsRights(rights, personalDays, usedDays) + } + if (isRequestingRights === YES) { + rights.push({ + rightsUnit: apiConstants.rights.receivingRightsId, + days: String(transferredDays), + rightsDescription: + rightsDescriptions[apiConstants.rights.receivingRightsId], + months: String( + getTransferredDaysInMonths(application, selectedChild), ), - approved: false, - paid: false, - rightsCodePeriod: basicRightCodePeriod, - }) - } else if (otherParent === SINGLE) { - // single parent - // Only using multiple births right - if (isSingleParentUsingMultipleBirthsRights) { - periods.push({ - from: period.startDate, - to: period.endDate, - ratio: getRatio( - period.ratio, - periodLength.toString(), - period.ratio === '100' ? isUsingNumberOfDays : true, - ), - approved: false, - paid: false, - rightsCodePeriod: mulitpleBirthsRights, - }) - } else { - /* - ** If we reach here, we have a period that will have: - ** 1: Personal rights and additional rights - ** 2: Personal, additional and multiplebirths rights - ** 3: Additional rights and multipleBirths rights - ** 4: Addtitonal rights - */ - if (maximumMultipleBirthsDaysToSpend === 0) { - if (isSingleParentUsingPersonalRights) { - // 1. Personal rights and additional rights - // Personal rights - const daysLeftOfPersonalRights = - maximumPersonalDaysToSpend - numberOfDaysAlreadySpent - const fromDate = getFromDate( - isFirstPeriod, - isActualDateOfBirth, - useLength, - period, - ) - - const personalPeriod = await this.getCalculatedPeriod( - nationalRegistryId, - startDate, - fromDate, - daysLeftOfPersonalRights, - period, - basicRightCodePeriod, - ) - - periods.push(personalPeriod) - - // Additional rights - const additionalSingleParentPeriodStartDate = addDays( - new Date(personalPeriod.to), - 1, - ) - const lengthOfPeriodUsingAdditionalSingleParentDays = - periodLength - daysLeftOfPersonalRights - - periods.push({ - from: format(additionalSingleParentPeriodStartDate, df), - to: period.endDate, - ratio: getRatio( - period.ratio, - lengthOfPeriodUsingAdditionalSingleParentDays.toString(), - true, - ), - approved: false, - paid: false, - rightsCodePeriod: - apiConstants.rights.artificialInseminationRightsId, - }) - } else { - // 4. Additional rights - periods.push({ - from: period.startDate, - to: period.endDate, - ratio: getRatio( - period.ratio, - periodLength.toString(), - period.ratio === '100' ? isUsingNumberOfDays : true, - ), - approved: false, - paid: false, - rightsCodePeriod: - apiConstants.rights.artificialInseminationRightsId, - }) - } - } else { - if (isSingleParentUsingPersonalRights) { - // 2. Personal, additional and multipleBirths rights - // Personal rights - const daysLeftOfPersonalRights = - maximumPersonalDaysToSpend - numberOfDaysAlreadySpent - const fromDate = getFromDate( - isFirstPeriod, - isActualDateOfBirth, - useLength, - period, - ) - - const personalPeriod = await this.getCalculatedPeriod( - nationalRegistryId, - startDate, - fromDate, - daysLeftOfPersonalRights, - period, - basicRightCodePeriod, - ) - - periods.push(personalPeriod) - - const additionalSingleParentPeriodStartDate = addDays( - new Date(personalPeriod.to), - 1, - ) - - // Additional rights - if (willSingleParentStartUsingMultipleBirthsRight) { - const additionalPeriod = await this.getCalculatedPeriod( - nationalRegistryId, - additionalSingleParentPeriodStartDate, - undefined, - maximumAdditionalSingleParentDaysToSpend, - period, - apiConstants.rights.artificialInseminationRightsId, - ) - - periods.push(additionalPeriod) - - // Common rights (multiple births) - const commonPeriodStartDate = addDays( - new Date(additionalPeriod.to), - 1, - ) - const lengthOfPeriodUsingCommonDays = - periodLength - - daysLeftOfPersonalRights - - maximumAdditionalSingleParentDaysToSpend - - periods.push({ - from: format(commonPeriodStartDate, df), - to: period.endDate, - ratio: getRatio( - period.ratio, - lengthOfPeriodUsingCommonDays.toString(), - true, - ), - approved: false, - paid: false, - rightsCodePeriod: mulitpleBirthsRights, - }) - } else { - // Additional rights - const lengthOfPeriodUsingAdditionalDays = - periodLength - daysLeftOfPersonalRights - - periods.push({ - from: format(additionalSingleParentPeriodStartDate, df), - to: period.endDate, - ratio: getRatio( - period.ratio, - lengthOfPeriodUsingAdditionalDays.toString(), - true, - ), - approved: false, - paid: false, - rightsCodePeriod: - apiConstants.rights.artificialInseminationRightsId, - }) - } - } else { - // 3. Additional rights and multipleBirths rights - if (willSingleParentStartUsingMultipleBirthsRight) { - // Additional rights - const lengthOfPeriodUsingAdditionalSingleParentDays = - maximumPersonalDaysToSpend + - maximumAdditionalSingleParentDaysToSpend - - numberOfDaysAlreadySpent - - const additionalPeriod = await this.getCalculatedPeriod( - nationalRegistryId, - startDate, - undefined, - lengthOfPeriodUsingAdditionalSingleParentDays, - period, - apiConstants.rights.artificialInseminationRightsId, - ) - - periods.push(additionalPeriod) - - // Common rights (multiple births) - const commonPeriodStartDate = addDays( - new Date(additionalPeriod.to), - 1, - ) - const lengthOfPeriodUsingCommonDays = - periodLength - lengthOfPeriodUsingAdditionalSingleParentDays - - periods.push({ - from: format(commonPeriodStartDate, df), - to: period.endDate, - ratio: getRatio( - period.ratio, - lengthOfPeriodUsingCommonDays.toString(), - true, - ), - approved: false, - paid: false, - rightsCodePeriod: mulitpleBirthsRights, - }) - } else { - // Only additional rights - periods.push({ - from: period.startDate, - to: period.endDate, - ratio: getRatio( - period.ratio, - periodLength.toString(), - period.ratio === '100' ? isUsingNumberOfDays : true, - ), - approved: false, - paid: false, - rightsCodePeriod: - apiConstants.rights.artificialInseminationRightsId, - }) - } - } - } - } - } else { - // has other parent - // We know all of the period will be using transferred rights - if (isUsingTransferredRights) { - periods.push({ - from: period.startDate, - to: period.endDate, - ratio: getRatio( - period.ratio, - periodLength.toString(), - period.ratio === '100' ? isUsingNumberOfDays : true, - ), - approved: false, - paid: false, - rightsCodePeriod: apiConstants.rights.receivingRightsId, - }) - } else if (willStartToUseTransferredRightsWithPeriod) { - /* - ** If we reach here, we have a period that will have to be split into - ** two, a part of it will be using personal/personal rights and the other part - ** will be using transferred rights - ** Case: - ** 1. Period includes personal rights and transfer rights - ** 2. Period includes common rights and transfer rights - ** 3. Period includes personal rights, common rights and transfer rights - */ - - // 1. Period includes personal and transfer rights - if (maximumMultipleBirthsDaysToSpend === 0) { - const fromDate = getFromDate( - isFirstPeriod, - isActualDateOfBirth, - useLength, - period, - ) - - // Personal - const daysLeftOfPersonalRights = - maximumPersonalDaysToSpend - numberOfDaysAlreadySpent - const personalPeriod = await this.getCalculatedPeriod( - nationalRegistryId, - startDate, - fromDate, - daysLeftOfPersonalRights, - period, - basicRightCodePeriod, - ) - - periods.push(personalPeriod) - - // Transferred - const transferredPeriodStartDate = addDays( - new Date(personalPeriod.to), - 1, - ) - const lengthOfPeriodUsingTransferredDays = - periodLength - daysLeftOfPersonalRights - - periods.push({ - from: format(transferredPeriodStartDate, df), - to: period.endDate, - ratio: getRatio( - period.ratio, - lengthOfPeriodUsingTransferredDays.toString(), - true, - ), - approved: false, - paid: false, - rightsCodePeriod: apiConstants.rights.receivingRightsId, - }) - } - // 2. Period includes common and transfer rights - else if (maximumPersonalDaysToSpend < numberOfDaysAlreadySpent) { - // Common (multiple births) - const daysLeftOfCommonRights = - maximumDaysBeforeUsingTransferRights - numberOfDaysAlreadySpent - const commonPeriod = await this.getCalculatedPeriod( - nationalRegistryId, - startDate, - undefined, - daysLeftOfCommonRights, - period, - mulitpleBirthsRights, - ) - - periods.push(commonPeriod) - - // Transferred - const transferredPeriodStartDate = addDays( - new Date(commonPeriod.to), - 1, - ) - const lengthOfPeriodUsingTransferredDays = - periodLength - daysLeftOfCommonRights - - periods.push({ - from: format(transferredPeriodStartDate, df), - to: period.endDate, - ratio: getRatio( - period.ratio, - lengthOfPeriodUsingTransferredDays.toString(), - true, - ), - approved: false, - paid: false, - rightsCodePeriod: apiConstants.rights.receivingRightsId, - }) - } - // 3. Period includes personal, common and transfer rights - else { - // Personal - const daysLeftOfPersonalRights = - maximumPersonalDaysToSpend - numberOfDaysAlreadySpent - const fromDate = getFromDate( - isFirstPeriod, - isActualDateOfBirth, - useLength, - period, - ) - - const personalPeriod = await this.getCalculatedPeriod( - nationalRegistryId, - startDate, - fromDate, - daysLeftOfPersonalRights, - period, - basicRightCodePeriod, - ) - - periods.push(personalPeriod) - - // Common - const commonPeriodStartDate = addDays( - new Date(personalPeriod.to), - 1, - ) - const commonPeriod = await this.getCalculatedPeriod( - nationalRegistryId, - commonPeriodStartDate, - undefined, - maximumMultipleBirthsDaysToSpend, - period, - mulitpleBirthsRights, - ) - - periods.push(commonPeriod) - // Transferred - const transferredPeriodStartDate = addDays( - new Date(commonPeriod.to), - 1, - ) - const lengthOfPeriodUsingTransferredDays = - periodLength - - daysLeftOfPersonalRights - - maximumMultipleBirthsDaysToSpend - - periods.push({ - from: format(transferredPeriodStartDate, df), - to: period.endDate, - ratio: getRatio( - period.ratio, - lengthOfPeriodUsingTransferredDays.toString(), - true, - ), - approved: false, - paid: false, - rightsCodePeriod: apiConstants.rights.receivingRightsId, - }) - } - } else if (isUsingMultipleBirthsRights) { - // Applicant used upp his/her basic rights and started to use 'common' rights - // and has not reach transfer rights - periods.push({ - from: period.startDate, - to: period.endDate, - ratio: getRatio( - period.ratio, - periodLength.toString(), - period.ratio === '100' ? isUsingNumberOfDays : true, + daysLeft: String( + clamp( + maximumPersonalDaysToSpend + + maximumMultipleBirthsDaysToSpend + + transferredDays - + usedDays, + 0, + transferredDays, ), - approved: false, - paid: false, - rightsCodePeriod: mulitpleBirthsRights, - }) - } else { - // If we reach here then there is personal rights mix with common rights - // Personal - const daysLeftOfPersonalRights = - maximumPersonalDaysToSpend - numberOfDaysAlreadySpent - const fromDate = getFromDate( - isFirstPeriod, - isActualDateOfBirth, - useLength, - period, - ) - const personalPeriod = await this.getCalculatedPeriod( - nationalRegistryId, - startDate, - fromDate, - daysLeftOfPersonalRights, - period, - basicRightCodePeriod, - ) + ), + }) + } + } - periods.push(personalPeriod) + return rights + } - // Common (multiple births) - const commonPeriodStartDate = addDays(new Date(personalPeriod.to), 1) - const lengthOfPeriodUsingCommonDays = - periodLength - daysLeftOfPersonalRights + calculatePeriodDays( + startDate: string, + endDate: string, + ratio: string, + daysToUse?: string, + months?: number, + ) { + if (daysToUse) { + return daysToUse + } + const start = parseISO(startDate) + const end = parseISO(endDate) + if (isNaN(start.getTime()) || isNaN(end.getTime())) { + throw new Error('Invalid startDate or endDate') + } + const ratioNumber = Number(ratio) + if (isNaN(ratioNumber) || ratioNumber <= 0) { + throw new Error('Invalid ratio value') + } + const percentage = ratioNumber / 100 + const periodLength = calculatePeriodLength(start, end, undefined, months) + return Math.round(periodLength * percentage) + } - periods.push({ - from: format(commonPeriodStartDate, df), - to: period.endDate, - ratio: getRatio( - period.ratio, - lengthOfPeriodUsingCommonDays.toString(), - true, - ), - approved: false, - paid: false, - rightsCodePeriod: mulitpleBirthsRights, - }) - } + createPeriodsDTO( + periods: AnswerPeriod[], + isActualDateOfBirth: boolean, + rights: string, + ): Period[] { + return periods.map((period, index) => { + const isFirstPeriod = index === 0 + return { + rightsCodePeriod: rights, + from: getFromDate( + isFirstPeriod, + isActualDateOfBirth, + period.useLength || '', + period, + ), + to: period.endDate, + ratio: `D${this.calculatePeriodDays( + period.startDate, + period.endDate, + period.ratio, + period.daysToUse, + period.months, + )}`, + approved: !!period.approved, + paid: !!period.paid, } + }) + } - // Add each period to the total number of days spent when an iteration is finished - numberOfDaysAlreadySpent += periodLength - } - - return periods + async preparePeriodsAndRightsDTO( + application: Application, + periods: AnswerPeriod[], + firstPeriodStart: string | undefined, + ): Promise<{ rightsDTO: ApplicationRights[]; periodsDTO: Period[] }> { + const rightsDTO = await this.createRightsDTO(application) + const rights = rightsDTO.map(({ rightsUnit }) => rightsUnit).join(',') + const isActualDateOfBirth = + firstPeriodStart === StartDateOptions.ACTUAL_DATE_OF_BIRTH + const periodsDTO = this.createPeriodsDTO( + periods, + isActualDateOfBirth, + rights, + ) + return { rightsDTO, periodsDTO } } async sendApplication({ @@ -1402,6 +909,8 @@ export class ParentalLeaveService extends BaseTemplateApiService { applicationType, employerLastSixMonths, employers, + periods, + firstPeriodStart, } = getApplicationAnswers(application.answers) // if ( // previousState === States.VINNUMALASTOFNUN_APPROVE_EDITS || @@ -1414,18 +923,20 @@ export class ParentalLeaveService extends BaseTemplateApiService { const attachments = await this.getAttachments(application) const type = getType(application) - try { - const periods = await this.createPeriodsDTO( - application, - nationalRegistryId, - ) + const { periodsDTO, rightsDTO } = await this.preparePeriodsAndRightsDTO( + application, + periods, + firstPeriodStart, + ) + try { const parentalLeaveDTO = transformApplicationToParentalLeaveDTO( application, - periods, + periodsDTO, attachments, - false, // put false in testData as this is not dummy request - type, + false, + getType(application), + rightsDTO, ) const response = @@ -1499,25 +1010,30 @@ export class ParentalLeaveService extends BaseTemplateApiService { async validateApplication({ application }: TemplateApiModuleActionProps) { const nationalRegistryId = application.applicant - const { previousState } = getApplicationAnswers(application.answers) + const { previousState, periods, firstPeriodStart } = getApplicationAnswers( + application.answers, + ) /* This is to avoid calling the api every time the user leaves the residenceGrantApplicationNoBirthDate state or residenceGrantApplication state */ // Reject from if (previousState === States.RESIDENCE_GRANT_APPLICATION_NO_BIRTH_DATE) { return } const attachments = await this.getAttachments(application) - try { - const periods = await this.createPeriodsDTO( - application, - nationalRegistryId, - ) + const { periodsDTO, rightsDTO } = await this.preparePeriodsAndRightsDTO( + application, + periods, + firstPeriodStart, + ) + + try { const parentalLeaveDTO = transformApplicationToParentalLeaveDTO( application, - periods, + periodsDTO, attachments, true, getType(application), + rightsDTO, ) // call SetParentalLeave API with testData: TRUE as this is a dummy request diff --git a/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.utils.ts b/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.utils.ts index 2e536a4f19cd..cda5b2b83eb6 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.utils.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/parental-leave/parental-leave.utils.ts @@ -28,6 +28,7 @@ import { ApplicationWithAttachments, } from '@island.is/application/types' import { + ApplicationRights, Attachment, Employer, ParentalLeave, @@ -371,6 +372,7 @@ export const transformApplicationToParentalLeaveDTO = ( | 'empdoc' | 'empdocper' | undefined, + applicationRights?: ApplicationRights[], ): ParentalLeave => { const selectedChild = getSelectedChild( application.answers, @@ -463,6 +465,7 @@ export const transformApplicationToParentalLeaveDTO = ( type, language: language === Languages.EN ? language : undefined, // Only send language if EN otherParentBlocked: otherParentRightOfAccess === NO ? true : false, + applicationRights, } } diff --git a/libs/application/templates/parental-leave/src/fields/Duration/Duration.tsx b/libs/application/templates/parental-leave/src/fields/Duration/Duration.tsx index f0f558c2b258..43ad11be12ce 100644 --- a/libs/application/templates/parental-leave/src/fields/Duration/Duration.tsx +++ b/libs/application/templates/parental-leave/src/fields/Duration/Duration.tsx @@ -80,8 +80,9 @@ export const Duration: FC> = ({ useEffect(() => { if (chosenEndDate) { setValue(`periods[${currentIndex}].endDate`, chosenEndDate) + setValue(`periods[${currentIndex}].months`, chosenDuration) } - }, [chosenEndDate, setValue, currentIndex]) + }, [chosenEndDate, setValue, currentIndex, chosenDuration]) const handleChange = async (months: number) => { clearErrors([id, 'component']) diff --git a/libs/application/templates/parental-leave/src/lib/directorateOfLabour.utils.ts b/libs/application/templates/parental-leave/src/lib/directorateOfLabour.utils.ts index 90d35e6265b9..5d85ff7875c8 100644 --- a/libs/application/templates/parental-leave/src/lib/directorateOfLabour.utils.ts +++ b/libs/application/templates/parental-leave/src/lib/directorateOfLabour.utils.ts @@ -154,7 +154,11 @@ export const calculatePeriodLength = ( start: Date, end: Date, percentage = 1, + months?: number, ) => { + if (months) { + return months * 30 + } if (end < start) { throw errorMessages.periodsEndDateBeforeStartDate } diff --git a/libs/application/templates/parental-leave/src/lib/parentalLeaveUtils.ts b/libs/application/templates/parental-leave/src/lib/parentalLeaveUtils.ts index a1c9b24c60c5..416e12901e3d 100644 --- a/libs/application/templates/parental-leave/src/lib/parentalLeaveUtils.ts +++ b/libs/application/templates/parental-leave/src/lib/parentalLeaveUtils.ts @@ -254,6 +254,30 @@ export const getTransferredDays = ( return days } +export const getPersonalDays = (application: Application) => { + const selectedChild = getSelectedChild( + application.answers, + application.externalData, + ) + if (!selectedChild) { + throw new Error('Missing selected child') + } + const maximumDaysToSpend = getAvailableRightsInDays(application) + const maximumMultipleBirthsDaysToSpend = getMultipleBirthsDays(application) + const maximumAdditionalSingleParentDaysToSpend = + getAdditionalSingleParentRightsInDays(application) + const transferredDays = getTransferredDays(application, selectedChild) + const personalDays = + maximumDaysToSpend - + maximumAdditionalSingleParentDaysToSpend - + maximumMultipleBirthsDaysToSpend - + Math.max(transferredDays, 0) + return personalDays +} + +export const getPersonalDaysInMonths = (application: Application) => + daysToMonths(getPersonalDays(application)).toFixed(1) + export const getMultipleBirthsDays = (application: Application) => { const selectedChild = getSelectedChild( application.answers, @@ -271,6 +295,9 @@ export const getMultipleBirthsDays = (application: Application) => { return getMultipleBirthRequestDays(application.answers) } +export const getMultipleBirthsDaysInMonths = (application: Application) => + daysToMonths(getMultipleBirthsDays(application)).toFixed(1) + export const getMultipleBirthRequestDays = ( answers: Application['answers'], ) => { @@ -388,6 +415,10 @@ export const getAvailablePersonalRightsSingleParentInMonths = ( getAdditionalSingleParentRightsInDays(application), ) +export const getAdditionalSingleParentRightsInMonths = ( + application: Application, +) => daysToMonths(getAdditionalSingleParentRightsInDays(application)).toFixed(1) + export const getAvailablePersonalRightsInMonths = (application: Application) => daysToMonths(getAvailablePersonalRightsInDays(application)) @@ -397,6 +428,11 @@ export const getAvailablePersonalRightsInMonths = (application: Application) => export const getAvailableRightsInMonths = (application: Application) => daysToMonths(getAvailableRightsInDays(application)) +export const getTransferredDaysInMonths = ( + application: Application, + selectedChild: ChildInformation, +) => daysToMonths(getTransferredDays(application, selectedChild)).toFixed(1) + export const getSpouse = ( application: Application, ): PersonInformation['spouse'] | null => { @@ -1355,13 +1391,21 @@ export const getMinimumStartDate = (application: Application): Date => { return today } +export const clamp = (value: number, min: number, max: number): number => + Math.min(max, Math.max(min, value)) + export const calculateDaysUsedByPeriods = (periods: Period[]) => Math.round( periods.reduce((total, period) => { const start = parseISO(period.startDate) const end = parseISO(period.endDate) const percentage = Number(period.ratio) / 100 - const periodLength = calculatePeriodLength(start, end) + const periodLength = calculatePeriodLength( + start, + end, + undefined, + period.months, + ) const calculatedLength = period.daysToUse ? Number(period.daysToUse) @@ -1386,7 +1430,7 @@ export const calculateEndDateForPeriodWithStartAndLength = ( const daysInMonth = getDaysInMonth(lastMonthBeforeEndDate) // If startDay is first day of the month and daysToAdd = 0 - if (daysToAdd === 0 && start.getDate() === 1) { + if (daysToAdd === 0) { return endDate } diff --git a/libs/application/templates/parental-leave/src/types.ts b/libs/application/templates/parental-leave/src/types.ts index f649525949fa..51f0bd006c79 100644 --- a/libs/application/templates/parental-leave/src/types.ts +++ b/libs/application/templates/parental-leave/src/types.ts @@ -80,6 +80,7 @@ export interface Period { rightCodePeriod?: string paid?: boolean approved?: boolean + months?: number } export interface Payment { diff --git a/libs/clients/vmst/src/clientConfig.yaml b/libs/clients/vmst/src/clientConfig.yaml index c41490cacc6c..3488c84eefaa 100644 --- a/libs/clients/vmst/src/clientConfig.yaml +++ b/libs/clients/vmst/src/clientConfig.yaml @@ -580,6 +580,10 @@ components: type: string otherParentBlocked: type: boolean + applicationRights: + type: array + items: + $ref: '#/components/schemas/ApplicationRights' PaymentInfo: type: object additionalProperties: false diff --git a/libs/clients/vmst/src/lib/utils.ts b/libs/clients/vmst/src/lib/utils.ts index 0f140250dbf8..bdf555ec9ec8 100644 --- a/libs/clients/vmst/src/lib/utils.ts +++ b/libs/clients/vmst/src/lib/utils.ts @@ -2,6 +2,7 @@ import fetch from 'isomorphic-fetch' import { logger } from '@island.is/logging' import omit from 'lodash/omit' import pick from 'lodash/pick' +import { ApplicationRights } from '../../gen/fetch' // A parental leave request body: type Init = { @@ -47,6 +48,7 @@ type Init = { }[] testData: string otherParentBlocked: boolean + applicationRights: ApplicationRights[] } export const createWrappedFetchWithLogging = ( @@ -79,6 +81,7 @@ export const createWrappedFetchWithLogging = ( // 'rightsCode', // 'status', 'testData', + 'applicationRights', ], ) // Then omit the sensitive sub-attributes From 18373d469e1ea6dd1bf583a466b86c1ff0de609c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafn=20=C3=81rnason?= Date: Mon, 7 Oct 2024 17:24:27 +0000 Subject: [PATCH 05/16] feat(passport-application): Updated readme (#16296) * updated readme * updated readme * chore: nx format:write update dirty files --------- Co-authored-by: andes-it Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- libs/application/templates/passport/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libs/application/templates/passport/README.md b/libs/application/templates/passport/README.md index 329270c0d408..52739d151797 100644 --- a/libs/application/templates/passport/README.md +++ b/libs/application/templates/passport/README.md @@ -5,3 +5,17 @@ This library was generated with [Nx](https://nx.dev). ## Running unit tests Run `ng test application-templates-passport` to execute the unit tests via [Jest](https://jestjs.io). + +## Environment Setup + +To properly run the application and go through the application process step by step, make sure to set the following environment variables: + +- `IDENTITY_SERVER_CLIENT_ID` +- `IDENTITY_SERVER_CLIENT_SECRET` + +## Test Users + +You can use the following test users during the application process: + +- **To apply for self**: 0101302989 +- **To apply for child**: 5555555559 From dcd0002c3056762d56a8aaeae3d222da7bbe5484 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9E=C3=B3r=C3=B0ur=20H?= Date: Mon, 7 Oct 2024 17:32:55 +0000 Subject: [PATCH 06/16] fix(regulations-admin): date format signature, remove self affect, disclaimer text (#16288) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../src/components/EditSignature.tsx | 6 +++--- .../components/impacts/ImpactBaseSelection.tsx | 3 +++ .../admin/regulations-admin/src/lib/messages.ts | 2 +- .../src/utils/formatAmendingRegulation.ts | 15 +-------------- .../src/utils/formatAmendingUtils.ts | 14 ++++++++++++++ .../admin/regulations-admin/src/utils/hooks.ts | 10 +++++----- 6 files changed, 27 insertions(+), 23 deletions(-) diff --git a/libs/portals/admin/regulations-admin/src/components/EditSignature.tsx b/libs/portals/admin/regulations-admin/src/components/EditSignature.tsx index 7d6a5089c3b8..8422c36d7e31 100644 --- a/libs/portals/admin/regulations-admin/src/components/EditSignature.tsx +++ b/libs/portals/admin/regulations-admin/src/components/EditSignature.tsx @@ -20,6 +20,7 @@ import { downloadUrl } from '../utils/files' import { DownloadDraftButton } from './DownloadDraftButton' import { useLocale } from '@island.is/localization' import { useS3Upload } from '../utils/dataHooks' +import { formatDate } from '../utils/formatAmendingUtils' // --------------------------------------------------------------------------- @@ -31,7 +32,6 @@ const defaultSignatureText = ` ` as HTMLText const getDefaultSignatureText = ( - dateFormatter: (date: Date, str?: string) => string, /** The ministry of the author-type user that created the RegulationDraft */ authorMinistry?: PlainText, ) => { @@ -41,7 +41,7 @@ const getDefaultSignatureText = ( const defaultMinister = '⸻ráðherra' return defaultSignatureText - .replace('{dags}', dateFormatter(new Date(), 'dd. MMMM yyyy')) + .replace('{dags}', formatDate(new Date())) .replace('{ministry}', authorMinistry || defaultMinistry) .replace('{minister}', authorMinister || defaultMinister) as HTMLText } @@ -159,7 +159,7 @@ export const EditSignature = () => { draftId={draft.id} value={ draft.signatureText.value || - getDefaultSignatureText(formatDateFns, draft.ministry.value) + getDefaultSignatureText(draft.ministry.value) } onChange={(text) => updateState('signatureText', text)} required={!!draft.signatureText.required} diff --git a/libs/portals/admin/regulations-admin/src/components/impacts/ImpactBaseSelection.tsx b/libs/portals/admin/regulations-admin/src/components/impacts/ImpactBaseSelection.tsx index 13c5a81887aa..338d8042e0fa 100644 --- a/libs/portals/admin/regulations-admin/src/components/impacts/ImpactBaseSelection.tsx +++ b/libs/portals/admin/regulations-admin/src/components/impacts/ImpactBaseSelection.tsx @@ -44,6 +44,9 @@ export const ImpactBaseSelection = ({ if (loading) { return null } + if (!mentionedOptions.length) { + return null + } return (