diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0f1602b5aadb..9727df0d2bf6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -109,6 +109,7 @@ libs/clients/rsk/relationships/ /libs/service-portal/sessions @island-is/aranja /apps/native/app/ @island-is/aranja-app codemagic.yaml @island-is/aranja-app +/apps/web/public/.well-known/ @island-is/aranja-app /apps/judicial-system/ @island-is/kolibri-justice-league /libs/judicial-system/ @island-is/kolibri-justice-league diff --git a/README.md b/README.md index 061eca71abcd..f5c26c5c8cde 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,8 @@ If you want to contribute to the repository, please make sure to follow [this gu - You have [gcc](https://gcc.gnu.org/) installed (Linux MacOs). - You have [g++](https://gcc.gnu.org/) installed (Linux MacOs). -{% hint style="info" %} -If you are running on Windows we recommend using [Docker and WSL2](https://docs.docker.com/desktop/windows/wsl/) -{% endhint %} +> [!NOTE] +> If you are running on Windows we recommend using [Docker and WSL2](https://docs.docker.com/desktop/windows/wsl/) ### For fetching secrets diff --git a/apps/application-system/api/infra/application-system-api.ts b/apps/application-system/api/infra/application-system-api.ts index 1e5cf164b74e..98e258a3d972 100644 --- a/apps/application-system/api/infra/application-system-api.ts +++ b/apps/application-system/api/infra/application-system-api.ts @@ -113,7 +113,7 @@ export const workerSetup = }) .resources({ limits: { cpu: '400m', memory: '768Mi' }, - requests: { cpu: '100m', memory: '384Mi' }, + requests: { cpu: '150m', memory: '384Mi' }, }) export const serviceSetup = (services: { diff --git a/apps/application-system/api/src/app/modules/application/utils/delegationUtils.ts b/apps/application-system/api/src/app/modules/application/utils/delegationUtils.ts index 015f6b99d4ae..76bfa4b0cd2e 100644 --- a/apps/application-system/api/src/app/modules/application/utils/delegationUtils.ts +++ b/apps/application-system/api/src/app/modules/application/utils/delegationUtils.ts @@ -1,10 +1,10 @@ import { User } from '@island.is/auth-nest-tools' import { Application } from '@island.is/application/types' -export function isNewActor( +export const isNewActor = ( application: Pick, user: User, -) { +) => { if (!user.actor) { return false } else if ( diff --git a/apps/application-system/api/src/app/modules/application/utils/tokenUtils.ts b/apps/application-system/api/src/app/modules/application/utils/tokenUtils.ts index fb8a6a1b8fdc..1f0bafa4a236 100644 --- a/apps/application-system/api/src/app/modules/application/utils/tokenUtils.ts +++ b/apps/application-system/api/src/app/modules/application/utils/tokenUtils.ts @@ -2,7 +2,7 @@ import jwt from 'jsonwebtoken' import { environment } from '../../../../environments' -export function verifyToken(token: string): T | null { +export const verifyToken = (token: string): T | null => { try { const decoded = jwt.verify( token, diff --git a/apps/application-system/form/src/utils/index.ts b/apps/application-system/form/src/utils/index.ts index 3acbefe05485..b16bdc57c94f 100644 --- a/apps/application-system/form/src/utils/index.ts +++ b/apps/application-system/form/src/utils/index.ts @@ -10,8 +10,8 @@ * - https://github.com/airbnb/lottie-web/issues/360#issuecomment-320243980 * */ -export function fixSvgUrls(_baseUrl: string) { - function fixForAttribute(attrib: string) { +export const fixSvgUrls = (_baseUrl: string) => { + const fixForAttribute = (attrib: string) => { const baseUrl = window.location.href /** diff --git a/apps/portals/admin/src/auth.ts b/apps/portals/admin/src/auth.ts index 4a081c7312ea..944cdc454551 100644 --- a/apps/portals/admin/src/auth.ts +++ b/apps/portals/admin/src/auth.ts @@ -38,6 +38,8 @@ if (userMocked) { AdminPortalScope.signatureCollectionProcess, AdminPortalScope.formSystem, AdminPortalScope.formSystemSuperUser, + AdminPortalScope.delegationSystem, + AdminPortalScope.delegationSystemAdmin, ], post_logout_redirect_uri: `${window.location.origin}`, userStorePrefix: 'ap.', diff --git a/apps/portals/admin/src/lib/masterNavigation.ts b/apps/portals/admin/src/lib/masterNavigation.ts index 5405d89aee5c..0a02cb5e4030 100644 --- a/apps/portals/admin/src/lib/masterNavigation.ts +++ b/apps/portals/admin/src/lib/masterNavigation.ts @@ -14,6 +14,7 @@ import { signatureCollectionNavigation } from '@island.is/portals/admin/signatur import { serviceDeskNavigation } from '@island.is/portals/admin/service-desk' import { petitionNavigation } from '@island.is/portals/admin/petition' import { formSystemNavigation } from '@island.is/form-system' +import { delegationAdminNav } from 'delegation-admin' export const rootNavigationItem: PortalNavigationItem = { name: coreMessages.overview, @@ -44,7 +45,10 @@ export const TOP_NAVIGATION: PortalNavigationItem = { serviceDeskNavigation, // Meðmælasöfnun signatureCollectionNavigation, + // Formakerfi formSystemNavigation, + // Aðgangsstýring umboð + delegationAdminNav, ], } export const BOTTOM_NAVIGATION: PortalNavigationItem = { diff --git a/apps/portals/admin/src/lib/modules.ts b/apps/portals/admin/src/lib/modules.ts index 73fc69898b39..9a840d14262d 100644 --- a/apps/portals/admin/src/lib/modules.ts +++ b/apps/portals/admin/src/lib/modules.ts @@ -10,6 +10,7 @@ import { petitionModule } from '@island.is/portals/admin/petition' import { serviceDeskModule } from '@island.is/portals/admin/service-desk' import { signatureCollectionModule } from '@island.is/portals/admin/signature-collection' import { formSystemModule } from '@island.is/form-system' +import { delegationAdminModule } from 'delegation-admin' /** * NOTE: @@ -30,4 +31,5 @@ export const modules: PortalModule[] = [ serviceDeskModule, signatureCollectionModule, formSystemModule, + delegationAdminModule, ] diff --git a/apps/services/auth/admin-api/infra/auth-admin-api.ts b/apps/services/auth/admin-api/infra/auth-admin-api.ts index 04b9465afa8b..439643e2763b 100644 --- a/apps/services/auth/admin-api/infra/auth-admin-api.ts +++ b/apps/services/auth/admin-api/infra/auth-admin-api.ts @@ -1,4 +1,21 @@ -import { json, service, ServiceBuilder } from '../../../../../infra/src/dsl/dsl' +import { + json, + ref, + service, + ServiceBuilder, +} from '../../../../../infra/src/dsl/dsl' + +const REDIS_NODE_CONFIG = { + dev: json([ + 'clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379', + ]), + staging: json([ + 'clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379', + ]), + prod: json([ + 'clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379', + ]), +} export const serviceSetup = (): ServiceBuilder<'services-auth-admin-api'> => { return service('services-auth-admin-api') @@ -25,10 +42,29 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-admin-api'> => { ]), prod: json(['https://innskra.island.is']), }, + XROAD_NATIONAL_REGISTRY_ACTOR_TOKEN: 'true', + XROAD_RSK_PROCURING_ACTOR_TOKEN: 'true', + XROAD_NATIONAL_REGISTRY_SERVICE_PATH: { + dev: 'IS-DEV/GOV/10001/SKRA-Protected/Einstaklingar-v1', + staging: 'IS-TEST/GOV/6503760649/SKRA-Protected/Einstaklingar-v1', + prod: 'IS/GOV/6503760649/SKRA-Protected/Einstaklingar-v1', + }, + XROAD_NATIONAL_REGISTRY_REDIS_NODES: REDIS_NODE_CONFIG, + XROAD_RSK_PROCURING_REDIS_NODES: REDIS_NODE_CONFIG, + COMPANY_REGISTRY_XROAD_PROVIDER_ID: { + dev: 'IS-DEV/GOV/10006/Skatturinn/ft-v1', + staging: 'IS-TEST/GOV/5402696029/Skatturinn/ft-v1', + prod: 'IS/GOV/5402696029/Skatturinn/ft-v1', + }, + COMPANY_REGISTRY_REDIS_NODES: REDIS_NODE_CONFIG, }) .secrets({ CLIENT_SECRET_ENCRYPTION_KEY: '/k8s/services-auth/admin-api/CLIENT_SECRET_ENCRYPTION_KEY', + IDENTITY_SERVER_CLIENT_SECRET: + '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET', + NATIONAL_REGISTRY_IDS_CLIENT_SECRET: + '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET', }) .ingress({ primary: { diff --git a/apps/services/auth/admin-api/src/app/app.module.ts b/apps/services/auth/admin-api/src/app/app.module.ts index 68b95ae3ed59..b409548fb8a2 100644 --- a/apps/services/auth/admin-api/src/app/app.module.ts +++ b/apps/services/auth/admin-api/src/app/app.module.ts @@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config' import { SequelizeModule } from '@nestjs/sequelize' import { + DelegationApiUserSystemNotificationConfig, DelegationConfig, SequelizeConfigService, } from '@island.is/auth-api-lib' @@ -24,6 +25,12 @@ import { ClientSecretsModule } from './v2/secrets/client-secrets.module' import { TenantsModule } from './v2/tenants/tenants.module' import { ScopesModule } from './v2/scopes/scopes.module' import { ProvidersModule } from './v2/providers/providers.module' +import { DelegationAdminModule } from './v2/delegations/delegation-admin.module' +import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships' +import { FeatureFlagConfig } from '@island.is/nest/feature-flags' +import { IdsClientConfig, XRoadConfig } from '@island.is/nest/config' +import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2' +import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry' @Module({ imports: [ @@ -46,9 +53,18 @@ import { ProvidersModule } from './v2/providers/providers.module' ProblemModule, ProvidersModule, ScopesModule, + DelegationAdminModule, ConfigModule.forRoot({ isGlobal: true, - load: [DelegationConfig], + load: [ + DelegationConfig, + RskRelationshipsClientConfig, + NationalRegistryClientConfig, + CompanyRegistryConfig, + FeatureFlagConfig, + XRoadConfig, + IdsClientConfig, + ], envFilePath: ['.env', '.env.secret'], }), ], diff --git a/apps/services/auth/admin-api/src/app/v2/delegations/delegation-admin.controller.ts b/apps/services/auth/admin-api/src/app/v2/delegations/delegation-admin.controller.ts new file mode 100644 index 000000000000..1889f4886baf --- /dev/null +++ b/apps/services/auth/admin-api/src/app/v2/delegations/delegation-admin.controller.ts @@ -0,0 +1,98 @@ +import { + Controller, + Delete, + Get, + Headers, + Param, + UseGuards, +} from '@nestjs/common' +import { ApiTags } from '@nestjs/swagger' + +import { + CurrentUser, + IdsUserGuard, + Scopes, + ScopesGuard, + User, +} from '@island.is/auth-nest-tools' +import { + DelegationAdminCustomDto, + DelegationAdminCustomService, +} from '@island.is/auth-api-lib' +import { Documentation } from '@island.is/nest/swagger' +import { Audit, AuditService } from '@island.is/nest/audit' +import { DelegationAdminScopes } from '@island.is/auth/scopes' +import flatMap from 'lodash/flatMap' +import { isDefined } from '@island.is/shared/utils' + +const namespace = '@island.is/auth/delegation-admin' + +@UseGuards(IdsUserGuard, ScopesGuard) +@Scopes(DelegationAdminScopes.read) +@ApiTags('delegation-admin') +@Controller('delegation-admin') +@Audit({ namespace }) +export class DelegationAdminController { + constructor( + private readonly delegationAdminService: DelegationAdminCustomService, + private readonly auditService: AuditService, + ) {} + + @Get() + @Documentation({ + response: { status: 200, type: DelegationAdminCustomDto }, + request: { + header: { + 'X-Query-National-Id': { + required: true, + description: 'fetch delegations for this national id', + }, + }, + }, + }) + @Audit({ + resources: (delegation) => + flatMap([ + ...delegation.incoming.map((d) => d.id ?? undefined), + ...delegation.outgoing.map((d) => d.id ?? undefined), + ]).filter(isDefined), + }) + async getDelegationAdmin( + @Headers('X-Query-National-Id') nationalId: string, + ): Promise { + return await this.delegationAdminService.getAllDelegationsByNationalId( + nationalId, + ) + } + + @Delete(':delegationId') + @Scopes(DelegationAdminScopes.admin) + @Documentation({ + response: { status: 204 }, + request: { + params: { + delegationId: { + required: true, + description: 'The id of the delegation to delete', + }, + }, + }, + }) + delete( + @CurrentUser() user: User, + @Param('delegationId') delegationId: string, + ) { + return this.auditService.auditPromise( + { + auth: user, + namespace, + action: 'delete', + resources: delegationId, + meta: (deleted) => ({ + deleted, + }), + }, + this.delegationAdminService.deleteDelegation(user, delegationId), + ) + } +} diff --git a/apps/services/auth/admin-api/src/app/v2/delegations/delegation-admin.module.ts b/apps/services/auth/admin-api/src/app/v2/delegations/delegation-admin.module.ts new file mode 100644 index 000000000000..09d0a1dd624d --- /dev/null +++ b/apps/services/auth/admin-api/src/app/v2/delegations/delegation-admin.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common' + +import { DelegationsModule as AuthDelegationsModule } from '@island.is/auth-api-lib' +import { FeatureFlagModule } from '@island.is/nest/feature-flags' +import { DelegationAdminController } from './delegation-admin.controller' + +@Module({ + imports: [AuthDelegationsModule, FeatureFlagModule], + controllers: [DelegationAdminController], + providers: [], +}) +export class DelegationAdminModule {} diff --git a/apps/services/auth/admin-api/src/app/v2/scopes/test/me-scopes.spec.ts b/apps/services/auth/admin-api/src/app/v2/scopes/test/me-scopes.spec.ts index 4a32c6d175cd..eea1e3012463 100644 --- a/apps/services/auth/admin-api/src/app/v2/scopes/test/me-scopes.spec.ts +++ b/apps/services/auth/admin-api/src/app/v2/scopes/test/me-scopes.spec.ts @@ -11,6 +11,7 @@ import { TranslatedValueDto, ApiScopeDelegationType, AdminPatchScopeDto, + ApiScope, } from '@island.is/auth-api-lib' import { FixtureFactory } from '@island.is/services/auth/testing' import { @@ -763,6 +764,7 @@ describe('MeScopesController', () => { let app: TestApp let server: request.SuperTest let apiScopeDelegationTypeModel: typeof ApiScopeDelegationType + let fixtureFactory: FixtureFactory beforeAll(async () => { app = await setupApp({ @@ -772,6 +774,7 @@ describe('MeScopesController', () => { dbType: 'postgres', }) server = request(app.getHttpServer()) + fixtureFactory = new FixtureFactory(app) apiScopeDelegationTypeModel = await app.get( getModelToken(ApiScopeDelegationType), @@ -886,6 +889,59 @@ describe('MeScopesController', () => { }, }) }) + + it('should only update requested delegation setting fields', async () => { + // Arrange + // Create new subject under testing test data to control initial state of delegation settings. + const sutScope = await fixtureFactory.createApiScope({ + domainName: TENANT_ID, + allowExplicitDelegationGrant: true, + supportedDelegationTypes: [AuthDelegationType.Custom], + }) + + // Act - Update partially delegation setting + const response = await server + .patch( + `/v2/me/tenants/${TENANT_ID}/scopes/${encodeURIComponent( + sutScope.name, + )}`, + ) + .send({ + addedDelegationTypes: [AuthDelegationType.ProcurationHolder], + }) + + // Assert that we only updated requested delegation setting fields + expect(response.status).toEqual(200) + expect(response.body).toMatchObject({ + ...sutScope.toDTO(), + displayName: [ + { + locale: 'is', + value: sutScope.displayName, + }, + ], + description: [ + { + locale: 'is', + value: sutScope.description, + }, + ], + grantToProcuringHolders: true, + supportedDelegationTypes: expect.arrayContaining([ + AuthDelegationType.Custom, + AuthDelegationType.ProcurationHolder, + ]), + } as AdminScopeDTO) + const apiScopeDelegationTypes = await apiScopeDelegationTypeModel.findAll( + { + where: { + apiScopeName: sutScope.name, + }, + }, + ) + + expect(apiScopeDelegationTypes).toHaveLength(2) + }) }) describe('POST: /v2/me/tenants/:tenantId/scopes', () => { diff --git a/apps/services/license-api/infra/license-api.ts b/apps/services/license-api/infra/license-api.ts index beed0f9cc795..586ded487c92 100644 --- a/apps/services/license-api/infra/license-api.ts +++ b/apps/services/license-api/infra/license-api.ts @@ -13,7 +13,7 @@ export const serviceSetup = (): ServiceBuilder<'license-api'> => .namespace('license-api') .resources({ limits: { cpu: '400m', memory: '512Mi' }, - requests: { cpu: '15m', memory: '256Mi' }, + requests: { cpu: '50m', memory: '256Mi' }, }) .env({ IDENTITY_SERVER_ISSUER_URL: { diff --git a/apps/skilavottord/web/components/CarDetailsBox/CarDetailsBox.tsx b/apps/skilavottord/web/components/CarDetailsBox/CarDetailsBox.tsx index 7b226468f4d2..08f270833fd8 100644 --- a/apps/skilavottord/web/components/CarDetailsBox/CarDetailsBox.tsx +++ b/apps/skilavottord/web/components/CarDetailsBox/CarDetailsBox.tsx @@ -2,19 +2,11 @@ import { Box, Stack, Text } from '@island.is/island-ui/core' import { OutlinedBox } from '@island.is/skilavottord-web/components' import React, { FC } from 'react' -import { useI18n } from '@island.is/skilavottord-web/i18n' - -import { InputController } from '@island.is/shared/form-fields' -import { Control, FieldValues } from 'react-hook-form' - interface BoxProps { vehicleId: string vehicleType: string modelYear: string vehicleOwner?: string | null - mileage?: number - control?: Control - showMileage?: boolean } export const CarDetailsBox: FC> = ({ @@ -22,16 +14,7 @@ export const CarDetailsBox: FC> = ({ vehicleType, modelYear, vehicleOwner, - mileage, - control, - showMileage, }) => { - const { - t: { - deregisterVehicle: { deregister: t }, - }, - } = useI18n() - return ( > = ({ > {vehicleId} - {showMileage && {mileage} km} {`${vehicleType}, ${modelYear}`} {vehicleOwner} - - {showMileage && ( - - - - )} ) diff --git a/apps/skilavottord/web/components/CarDetailsBox2/CarDetailsBox2.tsx b/apps/skilavottord/web/components/CarDetailsBox2/CarDetailsBox2.tsx new file mode 100644 index 000000000000..b76419da7cf3 --- /dev/null +++ b/apps/skilavottord/web/components/CarDetailsBox2/CarDetailsBox2.tsx @@ -0,0 +1,181 @@ +import { + AlertMessage, + Box, + Checkbox, + GridColumn, + GridContainer, + GridRow, + Stack, + Text, +} from '@island.is/island-ui/core' +import React, { FC, useState } from 'react' + +import { useI18n } from '@island.is/skilavottord-web/i18n' + +import { + InputController, + SelectController, +} from '@island.is/shared/form-fields' + +import { + OutInUsage, + PlateInfo, + UseStatus, +} from '@island.is/skilavottord-web/utils/consts' +import { Controller } from 'react-hook-form' + +interface BoxProps { + vehicleId: string + vehicleType: string + modelYear: string + mileage?: number + vehicleOwner?: string | null + vinNumber?: string + outInStatus: number + useStatus: string +} + +export const CarDetailsBox2: FC> = ({ + vehicleId, + vehicleType, + modelYear, + vehicleOwner, + vinNumber, + mileage, + outInStatus, + useStatus, +}) => { + const { + t: { + deregisterVehicle: { deregister: t }, + }, + } = useI18n() + + const [missingPlates, setMissingPlates] = useState(false) + const [lostPlate, setLostPlate] = useState(false) + + return ( + + + + + + + {vehicleId} + {mileage} km + {`${vehicleType}, ${modelYear}`} + {vinNumber && VIN: {vinNumber}} + {vehicleOwner} + + + + + + + + + + {t.numberplate.sectionTitle} + + + + {outInStatus === OutInUsage.OUT && ( + + + {useStatus !== UseStatus.OUT_TICKET && ( + + )} + {useStatus === UseStatus.OUT_TICKET && ( + + )} + + + )} + + {(outInStatus === OutInUsage.IN || + (outInStatus === OutInUsage.OUT && + useStatus === UseStatus.OUT_TICKET)) && ( + + + { + if (option?.value === 2) { + setMissingPlates(false) + setLostPlate(false) + } else { + setMissingPlates(true) + } + }} + defaultValue={2} + /> + + + )} + {missingPlates && ( + + + { + return ( + { + if (!lostPlate) { + onChange(PlateInfo.PLATE_LOST) + } else { + onChange() + } + + setLostPlate(!lostPlate) + }} + /> + ) + }} + /> + + + )} + + {lostPlate && ( + + + + + + )} + + + ) +} diff --git a/apps/skilavottord/web/components/CarDetailsBox2/index.ts b/apps/skilavottord/web/components/CarDetailsBox2/index.ts new file mode 100644 index 000000000000..9007f2a0b0b7 --- /dev/null +++ b/apps/skilavottord/web/components/CarDetailsBox2/index.ts @@ -0,0 +1 @@ +export * from './CarDetailsBox2' diff --git a/apps/skilavottord/web/components/InlineError/InlineError.tsx b/apps/skilavottord/web/components/InlineError/InlineError.tsx index 7078847692a9..77ad20b6cfb7 100644 --- a/apps/skilavottord/web/components/InlineError/InlineError.tsx +++ b/apps/skilavottord/web/components/InlineError/InlineError.tsx @@ -4,7 +4,7 @@ import { Box, Button, Icon, Stack, Text } from '@island.is/island-ui/core' export interface InlineErrorProps { title: string message: string - primaryButton: Button + primaryButton?: Button secondaryButton?: Button } @@ -40,12 +40,14 @@ export const InlineError: FC> = ({ {secondaryButton.text} )} - + {primaryButton && ( + + )} ) diff --git a/apps/skilavottord/web/components/index.ts b/apps/skilavottord/web/components/index.ts index bf898f6a3fe5..29e01cd92868 100644 --- a/apps/skilavottord/web/components/index.ts +++ b/apps/skilavottord/web/components/index.ts @@ -13,6 +13,7 @@ export * from './Sidenav/Sidenav' export * from './GDPR' export * from './Modal/Modal' export * from './CarDetailsBox/CarDetailsBox' +export * from './CarDetailsBox2/CarDetailsBox2' export * from './LinkProvider/LinkProvider' export * from './Layouts/AppLayout' export * from './Layouts/PageLayout' diff --git a/apps/skilavottord/web/i18n/locales/en.json b/apps/skilavottord/web/i18n/locales/en.json index 44b282fe1f66..cb3f7c2f3f2c 100644 --- a/apps/skilavottord/web/i18n/locales/en.json +++ b/apps/skilavottord/web/i18n/locales/en.json @@ -329,7 +329,23 @@ "primaryButton": "Try again", "secondaryButton": "Cancel" }, - "currentMileage": "Current mileage" + "currentMileage": "Current mileage", + "numberplate": { + "sectionTitle": "Number plates information", + "alert": { + "info": { + "title": "The vehicle is registered out of use at the Icelandic Transport Authority.", + "message": "The number plates are deposited at the Icelandic Transport Authority" + }, + "warning": { + "title": "The vehicle is registered out of use with a sticker at the Icelandic Transport Authority.", + "message": "The number plates must accompany the vehicle" + } + }, + "count": "Number of license plates returned with the vehicle", + "lost": "Number plates are missing", + "missingInfo": "If the number plates are found, they must be returned to the Icelandic Transport Authority" + } } }, "recyclingFundOverview": { diff --git a/apps/skilavottord/web/i18n/locales/is.json b/apps/skilavottord/web/i18n/locales/is.json index 7b4cfc5c7825..d8ec320474ca 100644 --- a/apps/skilavottord/web/i18n/locales/is.json +++ b/apps/skilavottord/web/i18n/locales/is.json @@ -329,7 +329,23 @@ "primaryButton": "Reyna aftur", "secondaryButton": "Hætta við" }, - "currentMileage": "Núverandi kílómetrastaða" + "currentMileage": "Núverandi kílómetrastaða", + "numberplate": { + "sectionTitle": "Upplýsingar um númeraplötur", + "alert": { + "info": { + "title": "Ökutækið er skráð úr umferð hjá Samgöngustofu", + "message": "Númeraplöturnar eru innlagðar hjá Samgöngustofu" + }, + "warning": { + "title": "Ökutækið er skráð úr umferð (miði) hjá Samgöngustofu", + "message": "Númeraplöturnar eiga að fylgja ökutækinu" + } + }, + "count": "Fjöldi númeraplatna skilað með ökutækinu", + "lost": "Númeraplötur eru týndar", + "missingInfo": "Ef að númeraplöturnar finnast skal skila þeim til Samgöngustofu" + } } }, "recyclingFundOverview": { diff --git a/apps/skilavottord/web/i18n/locales/translation.d.ts b/apps/skilavottord/web/i18n/locales/translation.d.ts index a43d1c7e44e9..28dd89be7eb0 100644 --- a/apps/skilavottord/web/i18n/locales/translation.d.ts +++ b/apps/skilavottord/web/i18n/locales/translation.d.ts @@ -273,6 +273,24 @@ export interface Deregister { success: string error: CompletedError currentMileage: string + numberplate: NumberPlate +} + +export interface NumberPlate { + sectionTitle: string + alert: DeregisteredMessages + count: string + lost: string + missingInfo: string +} + +export interface DeregisteredMessages { + info: Message + warning: Message +} +export interface Message { + title: string + message: string } export interface DeregisterButtons { diff --git a/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx b/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx index d89e2f6c7ea1..293ac53554ed 100644 --- a/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx +++ b/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx @@ -18,7 +18,7 @@ import { import { hasPermission } from '@island.is/skilavottord-web/auth/utils' import { - CarDetailsBox, + CarDetailsBox2, NotFound, OutlinedError, ProcessPageLayout, @@ -33,8 +33,9 @@ import { Role, } from '@island.is/skilavottord-web/graphql/schema' import { useI18n } from '@island.is/skilavottord-web/i18n' +import { OutInUsage } from '@island.is/skilavottord-web/utils/consts' import { getYear } from '@island.is/skilavottord-web/utils/dateUtils' -import { useForm } from 'react-hook-form' +import { FormProvider, useForm } from 'react-hook-form' const SkilavottordVehicleReadyToDeregisteredQuery = gql` query skilavottordVehicleReadyToDeregisteredQuery($permno: String!) { @@ -42,10 +43,22 @@ const SkilavottordVehicleReadyToDeregisteredQuery = gql` vehicleId vehicleType newregDate + vinNumber + mileage recyclingRequests { nameOfRequestor } - mileage + } + } +` + +const SkilavottordTrafficQuery = gql` + query skilavottordTrafficQuery($permno: String!) { + skilavottordTraffic(permno: $permno) { + permno + outInStatus + useStatus + useStatusName } } ` @@ -70,19 +83,27 @@ const SkilavottordRecyclingRequestMutation = gql` } ` -const UpdateSkilavottordVehicleMileageMutation = gql` - mutation updateSkilavottordVehicleMileage( +const UpdateSkilavottordVehicleInfoMutation = gql` + mutation updateSkilavottordVehicleInfo( $permno: String! $mileage: Float! + $plateCount: Float! + $plateLost: Boolean! ) { - updateSkilavottordVehicleMileage(permno: $permno, mileage: $mileage) + updateSkilavottordVehicleInfo( + permno: $permno + mileage: $mileage + plateCount: $plateCount + plateLost: $plateLost + ) } ` const Confirm: FC> = () => { - const { control, watch } = useForm({ + const methods = useForm({ mode: 'onChange', }) + const { watch } = methods const { user } = useContext(UserContext) const { @@ -95,6 +116,8 @@ const Confirm: FC> = () => { const { id } = router.query const mileageValue = watch('mileage') + const plateLost = watch('plateLost') + const plateCountValue = watch('plateCount') const { data, loading } = useQuery( SkilavottordVehicleReadyToDeregisteredQuery, @@ -105,6 +128,22 @@ const Confirm: FC> = () => { const vehicle = data?.skilavottordVehicleReadyToDeregistered + const { data: traffic, loading: loadingTraffic } = useQuery( + SkilavottordTrafficQuery, + { + variables: { permno: id }, + }, + ) + + const vehicleTrafficData = traffic?.skilavottordTraffic + + const outInStatus = + vehicleTrafficData?.outInStatus.toLocaleUpperCase() === 'OUT' + ? OutInUsage.OUT + : OutInUsage.IN + + const useStatus = vehicleTrafficData?.useStatus || '01' + const [ setRecyclingRequest, { data: mutationData, error: mutationError, loading: mutationLoading }, @@ -129,7 +168,7 @@ const Confirm: FC> = () => { error: vehicleMutationError, loading: vehicleMutationLoading, }, - ] = useMutation(UpdateSkilavottordVehicleMileageMutation, { + ] = useMutation(UpdateSkilavottordVehicleInfoMutation, { onError() { return vehicleMutationError }, @@ -144,28 +183,32 @@ const Confirm: FC> = () => { }, [vehicleMutationResponse, router, routes, t.success]) const handleConfirm = () => { - if (mileageValue !== undefined) { - const newMilage = +mileageValue.trim().replace(/\./g, '') + let newMileage = mileageValue - // If registered mileage is not the same as the one when vehicle is confirmed for de-registration we need to update it - if (vehicle?.mileage !== newMilage) { - setVehicleRequest({ - variables: { - permno: vehicle?.vehicleId, - mileage: newMilage, - }, - }) - } + if (mileageValue !== undefined) { + newMileage = +mileageValue.trim().replace(/\./g, '') + } else { + newMileage = vehicle?.mileage } - setRecyclingRequest({ + // Update vehicle table with latests information + setVehicleRequest({ variables: { - permno: id, - requestType: RecyclingRequestTypes.deregistered, + permno: vehicle?.vehicleId, + mileage: newMileage, + plateCount: plateCountValue === 0 ? 0 : plateCountValue, + plateLost: !!plateLost?.length, }, + }).then(() => { + // Send recycling request + setRecyclingRequest({ + variables: { + permno: id, + requestType: RecyclingRequestTypes.deregistered, + }, + }) }) } - const handleBack = () => { router.replace(routes.select) } @@ -221,22 +264,22 @@ const Confirm: FC> = () => { {t.titles.success} {t.info.success} - + + + ) : ( - {loading ? ( + {loading || loadingTraffic ? ( @@ -280,5 +323,4 @@ const Confirm: FC> = () => { ) } - export default Confirm diff --git a/apps/skilavottord/web/utils/consts.ts b/apps/skilavottord/web/utils/consts.ts index 675f1b64953d..3567ecffb468 100644 --- a/apps/skilavottord/web/utils/consts.ts +++ b/apps/skilavottord/web/utils/consts.ts @@ -1 +1,14 @@ export const ACCEPTED_TERMS_AND_CONDITION = 'acceptedTermsAndConditions' + +export enum PlateInfo { + PLATE_LOST = '0', +} + +export enum UseStatus { + OUT_TICKET = '16', // Úr umferð (miði) +} + +export enum OutInUsage { + OUT = 0, + IN = 1, +} diff --git a/apps/skilavottord/ws/migrations/20201008200611-vehicle_owner.js b/apps/skilavottord/ws/migrations/20201008200611-vehicle_owner.js index f4170a4404d7..f179c3da7af0 100644 --- a/apps/skilavottord/ws/migrations/20201008200611-vehicle_owner.js +++ b/apps/skilavottord/ws/migrations/20201008200611-vehicle_owner.js @@ -1,5 +1,3 @@ -'use strict' - module.exports.up = (queryInterface, DataTypes) => { return queryInterface.createTable( 'vehicle_owner', diff --git a/apps/skilavottord/ws/migrations/20201008210611-vehicle.js b/apps/skilavottord/ws/migrations/20201008210611-vehicle.js index 100f501066d6..7993f667c03f 100644 --- a/apps/skilavottord/ws/migrations/20201008210611-vehicle.js +++ b/apps/skilavottord/ws/migrations/20201008210611-vehicle.js @@ -1,4 +1,3 @@ -'use strict' // newreg_date TIMESTAMP WITH TIME ZONE DEFAULT now(), // vin_number VARCHAR, module.exports.up = (queryInterface, DataTypes) => { diff --git a/apps/skilavottord/ws/migrations/20201008210711-recycling-partners.js b/apps/skilavottord/ws/migrations/20201008210711-recycling-partners.js index b44b3ffe0fee..9823d0c2723e 100644 --- a/apps/skilavottord/ws/migrations/20201008210711-recycling-partners.js +++ b/apps/skilavottord/ws/migrations/20201008210711-recycling-partners.js @@ -1,5 +1,3 @@ -'use strict' - module.exports.up = (queryInterface, DataTypes) => { return queryInterface.createTable( 'recycling_partner', diff --git a/apps/skilavottord/ws/migrations/20201008220611-recycling_request.js b/apps/skilavottord/ws/migrations/20201008220611-recycling_request.js index d1eb3b08decc..d6c4e8e8e1f8 100644 --- a/apps/skilavottord/ws/migrations/20201008220611-recycling_request.js +++ b/apps/skilavottord/ws/migrations/20201008220611-recycling_request.js @@ -1,5 +1,3 @@ -'use strict' - module.exports.up = (queryInterface, DataTypes) => { return queryInterface.createTable( 'recycling_request', diff --git a/apps/skilavottord/ws/migrations/20201008230611-gdpr.js b/apps/skilavottord/ws/migrations/20201008230611-gdpr.js index 5af67b0129ea..cfd1cf8c7d14 100644 --- a/apps/skilavottord/ws/migrations/20201008230611-gdpr.js +++ b/apps/skilavottord/ws/migrations/20201008230611-gdpr.js @@ -1,5 +1,3 @@ -'use strict' - module.exports.up = (queryInterface, DataTypes) => { return queryInterface.createTable( 'gdpr', diff --git a/apps/skilavottord/ws/migrations/20211207210715-access-control.js b/apps/skilavottord/ws/migrations/20211207210715-access-control.js index 1ff3e3462013..3df1d4b2c6af 100644 --- a/apps/skilavottord/ws/migrations/20211207210715-access-control.js +++ b/apps/skilavottord/ws/migrations/20211207210715-access-control.js @@ -1,5 +1,3 @@ -'use strict' - module.exports.up = (queryInterface, DataTypes) => { return queryInterface.createTable( 'access_control', diff --git a/apps/skilavottord/ws/migrations/20220318210711-addcolumn-recycling-partners.js b/apps/skilavottord/ws/migrations/20220318210711-addcolumn-recycling-partners.js index f1b24e25d48c..90c9e7040264 100644 --- a/apps/skilavottord/ws/migrations/20220318210711-addcolumn-recycling-partners.js +++ b/apps/skilavottord/ws/migrations/20220318210711-addcolumn-recycling-partners.js @@ -1,5 +1,3 @@ -'use strict' - module.exports = { up: (queryInterface) => { return queryInterface.sequelize.query(` diff --git a/apps/skilavottord/ws/migrations/20220318210715-addcolumn-access-control.js b/apps/skilavottord/ws/migrations/20220318210715-addcolumn-access-control.js index 0e8d44c80476..63ce3c22fcbd 100644 --- a/apps/skilavottord/ws/migrations/20220318210715-addcolumn-access-control.js +++ b/apps/skilavottord/ws/migrations/20220318210715-addcolumn-access-control.js @@ -1,5 +1,3 @@ -'use strict' - module.exports = { up: (queryInterface) => { return queryInterface.sequelize.query(` diff --git a/apps/skilavottord/ws/migrations/20220318210715-alow-null-recycling-partners.js b/apps/skilavottord/ws/migrations/20220318210715-alow-null-recycling-partners.js index 1839c87917f4..111883805f16 100644 --- a/apps/skilavottord/ws/migrations/20220318210715-alow-null-recycling-partners.js +++ b/apps/skilavottord/ws/migrations/20220318210715-alow-null-recycling-partners.js @@ -1,5 +1,3 @@ -'use strict' - module.exports = { up: (queryInterface) => { return queryInterface.sequelize.query(` diff --git a/apps/skilavottord/ws/migrations/20231113210715-addcolumn-mileage.js b/apps/skilavottord/ws/migrations/20231113210715-addcolumn-mileage.js index 08959859c3af..8aa4b76bd3e2 100644 --- a/apps/skilavottord/ws/migrations/20231113210715-addcolumn-mileage.js +++ b/apps/skilavottord/ws/migrations/20231113210715-addcolumn-mileage.js @@ -1,5 +1,3 @@ -'use strict' - //add mileage change module.exports = { up: (queryInterface) => { diff --git a/apps/skilavottord/ws/migrations/20240307280301-add-indexes.js b/apps/skilavottord/ws/migrations/20240307280301-add-indexes.js index 68ec78a3044e..fced6fe46ab7 100644 --- a/apps/skilavottord/ws/migrations/20240307280301-add-indexes.js +++ b/apps/skilavottord/ws/migrations/20240307280301-add-indexes.js @@ -1,5 +1,3 @@ -'use strict' - //add mileage change module.exports = { up: (queryInterface) => { diff --git a/apps/skilavottord/ws/migrations/202409083210715-addcolumns-numberplate-info.js b/apps/skilavottord/ws/migrations/202409083210715-addcolumns-numberplate-info.js new file mode 100644 index 000000000000..4bfadb7ad42d --- /dev/null +++ b/apps/skilavottord/ws/migrations/202409083210715-addcolumns-numberplate-info.js @@ -0,0 +1,18 @@ +//add number plate & deregistered info change +module.exports = { + up: (queryInterface) => { + return queryInterface.sequelize.query(` + BEGIN; + ALTER TABLE vehicle ADD COLUMN plate_count INT; + ALTER TABLE vehicle ADD COLUMN plate_lost BOOLEAN; + COMMIT; + `) + }, + + down: (queryInterface) => { + return queryInterface.sequelize.query(` + ALTER TABLE vehicle DROP COLUMN plate_count; + ALTER TABLE vehicle DROP COLUMN plate_lost; + `) + }, +} diff --git a/apps/skilavottord/ws/src/app/modules/recyclingRequest/recyclingRequest.resolver.ts b/apps/skilavottord/ws/src/app/modules/recyclingRequest/recyclingRequest.resolver.ts index 0ba166e195d0..46d61f0d0a4c 100644 --- a/apps/skilavottord/ws/src/app/modules/recyclingRequest/recyclingRequest.resolver.ts +++ b/apps/skilavottord/ws/src/app/modules/recyclingRequest/recyclingRequest.resolver.ts @@ -76,11 +76,8 @@ export class RecyclingRequestResolver { @Args('recyclingPartner') station: string, @Args('mileage', { type: () => Int, nullable: true }) mileage: number, ): Promise { - return this.recyclingRequestService.deRegisterVehicle( - permno, - station, - mileage, - ) + //this.recyclingRequestService.deRegisterVehicle(permno, station, 0) + throw new Error('Not used anymore') } @Authorize({ diff --git a/apps/skilavottord/ws/src/app/modules/recyclingRequest/recyclingRequest.service.ts b/apps/skilavottord/ws/src/app/modules/recyclingRequest/recyclingRequest.service.ts index c72768a0509b..b84bedcff8ea 100644 --- a/apps/skilavottord/ws/src/app/modules/recyclingRequest/recyclingRequest.service.ts +++ b/apps/skilavottord/ws/src/app/modules/recyclingRequest/recyclingRequest.service.ts @@ -21,6 +21,8 @@ import { } from './recyclingRequest.model' import { VehicleService, VehicleModel } from '../vehicle' import { IcelandicTransportAuthorityServices } from '../../services/icelandicTransportAuthority.services' +import { ApiVersion } from '../../utils/const' +import { getShortPermno } from '../../utils/skilavottordUtils' @Injectable() export class RecyclingRequestService { @@ -39,11 +41,8 @@ export class RecyclingRequestService { async deRegisterVehicle( vehiclePermno: string, disposalStation: string, - mileage = 0, + vehicle: VehicleModel, ) { - const apiVersion = '3.0' - const apiVersionParam = '?api-version=' + apiVersion - try { const { restAuthUrl, restDeRegUrl, restUsername, restPassword } = environment.samgongustofa @@ -54,12 +53,12 @@ export class RecyclingRequestService { const jsonAuthBody = JSON.stringify(jsonObj) const headerAuthRequest = { 'Content-Type': 'application/json', - 'Api-version': apiVersion, + 'Api-version': ApiVersion.REGISTRATIONS, } // TODO: saved jToken and use it in next 7 days ( until it expires ) const authRes = await lastValueFrom( - this.httpService.post(restAuthUrl + apiVersionParam, jsonAuthBody, { + this.httpService.post(restAuthUrl, jsonAuthBody, { headers: headerAuthRequest, }), ) @@ -68,26 +67,31 @@ export class RecyclingRequestService { this.logger.error(errorMessage) throw new Error(errorMessage) } + // DeRegisterd vehicle const jToken = authRes.data['jwtToken'] const jsonDeRegBody = JSON.stringify({ permno: vehiclePermno, deRegisterDate: format(new Date(), "yyyy-MM-dd'T'HH:mm:ss"), - disposalstation: disposalStation, + disposalStation: disposalStation, explanation: 'Rafrænt afskráning', - mileage: mileage, + mileage: vehicle.mileage ?? 0, + plateCount: vehicle.plateCount, + lost: vehicle.plateLost ? 1 : 0, }) + const headerDeRegRequest = { 'Content-Type': 'application/json', Authorization: 'Bearer ' + jToken, - 'Api-version': apiVersion, + 'Api-version': ApiVersion.REGISTRATIONS, } const deRegRes = await lastValueFrom( - this.httpService.post(restDeRegUrl + apiVersionParam, jsonDeRegBody, { + this.httpService.post(restDeRegUrl, jsonDeRegBody, { headers: headerDeRegRequest, }), ) + if (deRegRes.status < 300 && deRegRes.status >= 200) { return true } else { @@ -100,7 +104,9 @@ export class RecyclingRequestService { error.config.data = undefined } this.logger.error(`Failed to deregister vehicle`, { error }) - throw new Error(`Failed to deregister vehicle ${vehiclePermno.slice(-3)}`) + throw new Error( + `Failed to deregister vehicle ${getShortPermno(vehiclePermno)}`, + ) } } @@ -139,7 +145,7 @@ export class RecyclingRequestService { permno: string, ): Promise { // We are only logging the last 3 chars in the vehicle number - const loggedPermno = permno.slice(-3) + const loggedPermno = getShortPermno(permno) try { // Check 'pendingRecycle' status @@ -185,7 +191,7 @@ export class RecyclingRequestService { const errors = new RequestErrors() // We are only logging the last 3 chars in the vehicle number - const loggedPermno = permno.slice(-3) + const loggedPermno = getShortPermno(permno) this.logger.info(`car-recycling: Recycling request ${loggedPermno}`, { requestType: requestType, @@ -344,12 +350,14 @@ export class RecyclingRequestService { this.logger.info( `car-recycling: Degregistering vehicle ${loggedPermno} from Samgongustofa`, { - mileage: vehicle.mileage ?? 0, partnerId, + mileage: vehicle.mileage ?? 0, + plateCount: vehicle.plateCount, + lost: vehicle.plateLost ? 1 : 0, }, ) - await this.deRegisterVehicle(permno, partnerId, vehicle.mileage ?? 0) + await this.deRegisterVehicle(permno, partnerId, vehicle) } catch (err) { // Saved requestType back to 'pendingRecycle' const req = new RecyclingRequestModel() diff --git a/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.model.ts b/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.model.ts index 74e49c8d5db7..67ffc5b2c271 100644 --- a/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.model.ts +++ b/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.model.ts @@ -46,3 +46,57 @@ export class VehicleInformation { @Field() status: string } + +@ObjectType() +export class VehicleInformationMini { + constructor( + permno: string, + ownerSocialSecurityNumber: string, + vehicleStatus: string, + ) { + this.permno = permno + this.ownerSocialSecurityNumber = ownerSocialSecurityNumber + this.vehicleStatus = vehicleStatus + } + + @Field() + permno: string + + @Field() + ownerSocialSecurityNumber: string + + @Field() + vehicleStatus: string +} + +@ObjectType() +export class Traffic { + constructor( + permno: string, + outInStatus: string, + useStatus: string, + useStatusName: string, + useDate: string, + ) { + this.permno = permno + this.outInStatus = outInStatus + this.useStatus = useStatus + this.useStatusName = useStatusName + this.useDate = useDate + } + + @Field() + permno: string + + @Field() + outInStatus: string + + @Field() + useStatus: string + + @Field() + useStatusName: string + + @Field() + useDate: string +} diff --git a/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.module.ts b/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.module.ts index 5f4511c1a420..ccee791fd703 100644 --- a/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.module.ts +++ b/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.module.ts @@ -5,10 +5,11 @@ import { RecyclingRequestModule } from '../recyclingRequest/recyclingRequest.mod import { SamgongustofaService } from './samgongustofa.service' import { SamgongustofaResolver } from './samgongustofa.resolver' +import { TransportService } from './transport/transport.service' @Module({ imports: [HttpModule, forwardRef(() => RecyclingRequestModule)], - providers: [SamgongustofaResolver, SamgongustofaService], + providers: [SamgongustofaResolver, SamgongustofaService, TransportService], exports: [SamgongustofaService], }) export class SamgongustofaModule {} diff --git a/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.resolver.ts b/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.resolver.ts index 2c87339b8508..5e343fc54532 100644 --- a/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.resolver.ts +++ b/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.resolver.ts @@ -1,6 +1,6 @@ -import { Query, Resolver } from '@nestjs/graphql' +import { Args, Query, Resolver } from '@nestjs/graphql' import { Authorize, CurrentUser, User } from '../auth' -import { VehicleInformation } from './samgongustofa.model' +import { Traffic, VehicleInformation } from './samgongustofa.model' import { SamgongustofaService } from './samgongustofa.service' @Authorize() @@ -14,4 +14,12 @@ export class SamgongustofaResolver { ): Promise> { return this.samgongustofaService.getUserVehiclesInformation(user.nationalId) } + + @Query(() => Traffic) + async skilavottordTraffic( + @CurrentUser() user: User, + @Args('permno') permno: string, + ): Promise { + return this.samgongustofaService.getTraffic(permno) + } } diff --git a/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.service.ts b/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.service.ts index 87610c34a5d3..58bd1fc96afd 100644 --- a/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.service.ts +++ b/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.service.ts @@ -1,12 +1,16 @@ -import { Injectable, Inject, forwardRef } from '@nestjs/common' import { HttpService } from '@nestjs/axios' +import { forwardRef, Inject, Injectable } from '@nestjs/common' import { lastValueFrom } from 'rxjs' import * as xml2js from 'xml2js' import { environment } from '../../../environments' import { RecyclingRequestService } from '../recyclingRequest' -import { VehicleInformation } from './samgongustofa.model' +import { Traffic, VehicleInformation } from './samgongustofa.model' + import { logger } from '@island.is/logging' +import { TransportService } from './transport/transport.service' +import { ApiVersion } from '../../utils/const' +import { getShortPermno } from '../../utils/skilavottordUtils' @Injectable() export class SamgongustofaService { @@ -14,8 +18,15 @@ export class SamgongustofaService { private httpService: HttpService, @Inject(forwardRef(() => RecyclingRequestService)) private recyclingRequestService: RecyclingRequestService, + private transportService: TransportService, ) {} + /** + * + * @param nationalId + * @deprecated + * @returns + */ async getUserVehiclesInformation( nationalId: string, ): Promise { @@ -306,6 +317,9 @@ export class SamgongustofaService { } } + /** + * @deprecated + */ async getUserVehicle( nationalId: string, permno: string, @@ -321,4 +335,65 @@ export class SamgongustofaService { } return car } + + async getTraffic(permno: string): Promise { + try { + const url = this.transportService.getRegistrationURL() + + const result = await this.transportService.doGet( + url, + url + 'traffic/' + permno, + ApiVersion.REGISTRATIONS, + ) + + if (result.status === 200) { + // Get the latest registered traffic data + const traffic = Object.values(result.data).reduce( + (prev: Traffic, current: Traffic) => + new Date(prev.useDate) > new Date(current.useDate) ? prev : current, + {} as Traffic, + ) as Traffic + + logger.info( + `car-recycling: Got traffic data for ${getShortPermno(permno)}`, + { + outInStatus: traffic.outInStatus, + useStatus: traffic.useStatus, + useStatusName: traffic.useStatusName, + }, + ) + + return traffic + } + + throw new Error( + `car-recycling: #1 Failed on getTraffic ${getShortPermno(permno)}`, + ) + } catch (err) { + throw new Error( + `car-recycling: #2 Failed on getTraffic ${getShortPermno( + permno, + )} because: ${err}`, + ) + } + } + + // Get the Vehicle information from Icelandic Transport Authority (Samgöngustofa) + async getVehicleInformation(permno: string) { + try { + const url = this.transportService.getInformationURL() + + return this.transportService.doGet( + url, + url + 'vehicleinformationmini/' + permno, + ApiVersion.INFORMATION, + ) + } catch (err) { + throw new Error( + `car-recycling: Failed on getVehicleInformation vehicle ${permno.slice( + -3, + )} because: ${err}`, + ) + } + } } diff --git a/apps/skilavottord/ws/src/app/modules/samgongustofa/test/samgongustofa.service.spec.ts b/apps/skilavottord/ws/src/app/modules/samgongustofa/test/samgongustofa.service.spec.ts index 45ff1cf5d3b8..9f0cfc08a388 100644 --- a/apps/skilavottord/ws/src/app/modules/samgongustofa/test/samgongustofa.service.spec.ts +++ b/apps/skilavottord/ws/src/app/modules/samgongustofa/test/samgongustofa.service.spec.ts @@ -10,6 +10,7 @@ import { import { SamgongustofaService } from '../samgongustofa.service' import { MockData } from './mock-data' +import { TransportService } from '../transport/transport.service' const recyclingRequestModel = { id: '1234', @@ -50,6 +51,12 @@ describe('skilavottordApiTest', () => { findAllWithPermno: () => ({}), })), }, + { + provide: TransportService, + useClass: jest.fn(() => ({ + getRegistrationURL: () => ({}), + })), + }, ], }).compile() samgongustofaService = diff --git a/apps/skilavottord/ws/src/app/modules/samgongustofa/transport/index.ts b/apps/skilavottord/ws/src/app/modules/samgongustofa/transport/index.ts new file mode 100644 index 000000000000..8d7c134af4ac --- /dev/null +++ b/apps/skilavottord/ws/src/app/modules/samgongustofa/transport/index.ts @@ -0,0 +1 @@ +export { TransportService } from './transport.service' diff --git a/apps/skilavottord/ws/src/app/modules/samgongustofa/transport/transport.service.ts b/apps/skilavottord/ws/src/app/modules/samgongustofa/transport/transport.service.ts new file mode 100644 index 000000000000..5ec134cb39f0 --- /dev/null +++ b/apps/skilavottord/ws/src/app/modules/samgongustofa/transport/transport.service.ts @@ -0,0 +1,111 @@ +import { Injectable } from '@nestjs/common' +import { HttpService } from '@nestjs/axios' +import { lastValueFrom } from 'rxjs' +import { environment } from '../../../../environments' +import { logger } from '@island.is/logging' + +@Injectable() +export class TransportService { + constructor(private httpService: HttpService) {} + + async authenticate(url: string, apiVersion = '1.0'): Promise { + try { + const { restUsername, restPassword } = environment.samgongustofa + + if (!restUsername || !restPassword) { + throw new Error('Missing environment variables for Samgöngustofa') + } + + const jsonObj = { + username: restUsername, + password: restPassword, + } + const jsonAuthBody = JSON.stringify(jsonObj) + + const headerAuthRequest = { + 'Content-Type': 'application/json', + 'Api-version': apiVersion, + } + + const authRes = await lastValueFrom( + this.httpService.post(url + 'authenticate', jsonAuthBody, { + headers: headerAuthRequest, + }), + ) + + if (authRes.status > 299 || authRes.status < 200) { + const errorMessage = `Authentication failed to Samgöngustofa services: ${authRes.statusText}` + logger.error(`car-recycling: ${errorMessage}`) + throw new Error(errorMessage) + } + + return authRes.data['jwtToken'] + } catch (error) { + if (error?.config) { + error.config.data = undefined + } + + logger.error('car-recycling: Authentication failed', error) + throw error + } + } + + /** + * + * @param restURL - the base Samgöngustofa API url + * @param fullUrl - the full url to the Samgöngustofa API endpoint + * @param apiVersion - Samgöngustofa API version + * @returns + */ + async doGet(restURL: string, fullUrl: string, apiVersion: string) { + const jwtToken = await this.authenticate(restURL, apiVersion) + + const headers = { + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + jwtToken, + 'Api-version': apiVersion, + } + + const result = await lastValueFrom( + this.httpService.get(fullUrl, { + headers: headers, + }), + ) + + if (result.status < 300 && result.status >= 200) { + return result + } else { + throw new Error( + `car-recycling: Failed on doGet with status: ${result.statusText}`, + ) + } + } + + /** + * Get the Samgöngustofa's registration REST url + * @returns + */ + getRegistrationURL(): string { + const { restDeRegUrl } = environment.samgongustofa + + const positionOfChar = restDeRegUrl.lastIndexOf('/') + return restDeRegUrl.substring(0, positionOfChar) + '/' + } + + /** + * Get the Samgöngustofa's information REST url + * @returns + */ + getInformationURL(): string { + const { restDeRegUrl } = environment.samgongustofa + + // + // Small hack to get the information url + const restInformationURL = restDeRegUrl.replace( + '/registrations/', + '/information/', + ) + const positionOfChar = restInformationURL.lastIndexOf('/') + return restInformationURL.substring(0, positionOfChar) + '/' + } +} diff --git a/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.model.ts b/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.model.ts index fb9a15d3b634..ad6e8bc929f3 100644 --- a/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.model.ts +++ b/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.model.ts @@ -80,6 +80,18 @@ export class VehicleModel extends Model { @Field(() => [RecyclingRequestModel], { nullable: true }) @HasMany(() => RecyclingRequestModel) recyclingRequests!: RecyclingRequestModel[] + + @Field({ nullable: true }) + @Column({ + type: DataType.BOOLEAN, + }) + plateLost?: boolean + + @Field({ nullable: true }) + @Column({ + type: DataType.INTEGER, + }) + plateCount?: number } @ObjectType() diff --git a/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.resolver.ts b/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.resolver.ts index b52c8fd483c2..a5f2af14ec32 100644 --- a/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.resolver.ts +++ b/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.resolver.ts @@ -104,11 +104,18 @@ export class VehicleResolver { } @Mutation(() => Boolean) - async updateSkilavottordVehicleMileage( + async updateSkilavottordVehicleInfo( @CurrentUser() user: User, @Args('permno') permno: string, @Args('mileage') mileage: number, + @Args('plateCount') plateCount: number, + @Args('plateLost') plateLost: boolean, ) { - return await this.vehicleService.updateMileage(permno, mileage) + return await this.vehicleService.updateVehicleInfo( + permno, + mileage, + plateCount, + plateLost, + ) } } diff --git a/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.service.ts b/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.service.ts index ce62cce725bb..3be43b3a65b8 100644 --- a/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.service.ts +++ b/apps/skilavottord/ws/src/app/modules/vehicle/vehicle.service.ts @@ -60,18 +60,27 @@ export class VehicleService { } } - async updateMileage(permno: string, mileage: number): Promise { + async updateVehicleInfo( + permno: string, + mileage: number, + plateCount: number, + plateLost: boolean, + ): Promise { const findVehicle = await this.findByVehicleId(permno) if (findVehicle) { findVehicle.mileage = mileage ?? 0 + findVehicle.plateCount = plateCount ?? 0 + findVehicle.plateLost = plateLost + await findVehicle.save() return true } else { - const errorMsg = `failed to update mileage: ${mileage} on vehicle: ${permno}` + const errorMsg = `Failed to update vehicleInfo for vehicle: ${permno}` this.logger.error( - `car-recycling: Failed to update mileage: ${mileage} on vehicle: ${permno.slice( + `car-recycling: Failed to update vehicleInfo for vehicle: ${permno.slice( -3, )}`, + { mileage, plateCount, plateLost }, ) throw new Error(errorMsg) } diff --git a/apps/skilavottord/ws/src/app/modules/vehicle/vehicleAppSys.resolver.ts b/apps/skilavottord/ws/src/app/modules/vehicle/vehicleAppSys.resolver.ts index ddf2133b3f9c..d15060f93a12 100644 --- a/apps/skilavottord/ws/src/app/modules/vehicle/vehicleAppSys.resolver.ts +++ b/apps/skilavottord/ws/src/app/modules/vehicle/vehicleAppSys.resolver.ts @@ -8,6 +8,7 @@ import { logger } from '@island.is/logging' import { CreateVehicleInput } from './dto/createVehicle.input' import { VehicleModel } from './vehicle.model' import { VehicleService } from './vehicle.service' +import { getShortPermno } from '../../utils/skilavottordUtils' @UseGuards(IdsUserGuard, ScopesGuard) @Resolver(() => VehicleModel) @@ -19,9 +20,12 @@ export class VehicleAppSysResolver { @CurrentUser() user: User, @Args('input') input: CreateVehicleInput, ) { - logger.info(`car-recycling: Creating Vehicle ${input.permno.slice(-3)}`, { - mileage: input.mileage, - }) + logger.info( + `car-recycling: Creating Vehicle ${getShortPermno(input.permno)}`, + { + mileage: input.mileage, + }, + ) const newVehicle = new VehicleModel() newVehicle.vinNumber = input.vin diff --git a/apps/skilavottord/ws/src/app/services/icelandicTransportAuthority.module.ts b/apps/skilavottord/ws/src/app/services/icelandicTransportAuthority.module.ts index 8207430e01de..e5fecff38762 100644 --- a/apps/skilavottord/ws/src/app/services/icelandicTransportAuthority.module.ts +++ b/apps/skilavottord/ws/src/app/services/icelandicTransportAuthority.module.ts @@ -1,13 +1,18 @@ import { Module, forwardRef } from '@nestjs/common' import { IcelandicTransportAuthorityServices } from './icelandicTransportAuthority.services' import { HttpModule } from '@nestjs/axios' -import { VehicleModule, VehicleOwnerModule } from '../modules' +import { + SamgongustofaModule, + VehicleModule, + VehicleOwnerModule, +} from '../modules' @Module({ imports: [ HttpModule, forwardRef(() => VehicleModule), forwardRef(() => VehicleOwnerModule), + forwardRef(() => SamgongustofaModule), ], providers: [IcelandicTransportAuthorityServices], exports: [IcelandicTransportAuthorityServices], diff --git a/apps/skilavottord/ws/src/app/services/icelandicTransportAuthority.services.ts b/apps/skilavottord/ws/src/app/services/icelandicTransportAuthority.services.ts index d56f9873585e..24254b673c06 100644 --- a/apps/skilavottord/ws/src/app/services/icelandicTransportAuthority.services.ts +++ b/apps/skilavottord/ws/src/app/services/icelandicTransportAuthority.services.ts @@ -1,8 +1,7 @@ -import { Injectable } from '@nestjs/common/decorators/core' -import { lastValueFrom } from 'rxjs' -import { HttpService } from '@nestjs/axios/dist' -import { environment } from '../../environments' import { logger } from '@island.is/logging' +import { Injectable } from '@nestjs/common/decorators/core' +import { SamgongustofaService } from '../modules/samgongustofa' +import { VehicleInformationMini } from '../modules/samgongustofa/samgongustofa.model' import { VehicleModel, VehicleService } from '../modules/vehicle' import { VehicleOwnerModel } from '../modules/vehicleOwner' import { VehicleOwnerService } from '../modules/vehicleOwner/vehicleOwner.service' @@ -10,124 +9,14 @@ import { VehicleOwnerService } from '../modules/vehicleOwner/vehicleOwner.servic @Injectable() export class IcelandicTransportAuthorityServices { constructor( - private http: HttpService, private vehicleService: VehicleService, private ownerService: VehicleOwnerService, + private samgongustofaService: SamgongustofaService, ) {} - // Hack to re-use the url from the secret - private getInformationURL(restURL: string): string { - const restInformationURL = restURL.replace( - '/registrations/', - '/information/', - ) - const positionOfChar = restInformationURL.lastIndexOf('/') - return restInformationURL.substring(0, positionOfChar) + '/' - } - - async authenticate(authURL: string, userName: string, password: string) { - try { - const jsonObj = { - username: userName, - password: password, - } - const jsonAuthBody = JSON.stringify(jsonObj) - - const headerAuthRequest = { - 'Content-Type': 'application/json', - } - - const authRes = await lastValueFrom( - this.http.post( - this.getInformationURL(authURL) + 'authenticate', - jsonAuthBody, - { - headers: headerAuthRequest, - }, - ), - ) - - if (authRes.status > 299 || authRes.status < 200) { - const errorMessage = `Authentication failed to information services: ${authRes.statusText}` - logger.error(`car-recycling: ${errorMessage}`) - throw new Error(errorMessage) - } - - return authRes.data['jwtToken'] - } catch (error) { - if (error?.config) { - error.config.data = undefined - } - logger.error('car-recycling: Authentication failed on information', error) - throw error - } - } - - async doGet( - restURL: string, - queryParams: { [key: string]: string } | undefined, - ) { - const { restAuthUrl, restUsername, restPassword } = - environment.samgongustofa - - const jwtToken = await this.authenticate( - restAuthUrl, - restUsername, - restPassword, - ) - - let fullUrl = restURL - - if (queryParams) { - const searchParams = new URLSearchParams(queryParams) - - // Concatenate the URL with the query string - fullUrl = `${restURL}?${searchParams.toString()}` - } - - const headers = { - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + jwtToken, - } - - const result = await lastValueFrom( - this.http.get(fullUrl, { - headers: headers, - }), - ) - - if (result.status < 300 && result.status >= 200) { - return result - } else { - throw new Error( - `car-recycling: Failed on doGet with status: ${result.statusText}`, - ) - } - } - - // Get the Vehicle information from Icelandic Transport Authority (Samgöngustofa) - async getVehicleInformation(permno: string) { - try { - const { restDeRegUrl } = environment.samgongustofa - - return this.doGet( - this.getInformationURL(restDeRegUrl) + - 'vehicleinformationmini/' + - permno, - undefined, - ) - } catch (err) { - throw new Error( - `car-recycling: Failed on getVehicleInformation vehicle ${permno.slice( - -3, - )} because: ${err}`, - ) - } - } - - async checkIfCurrentUser(permno: string): Promise { + async checkIfCurrentUser(permno: string): Promise { //Get the latest vehicle information from Samgongustofa - const result = await this.getVehicleInformation(permno) + const result = await this.samgongustofaService.getVehicleInformation(permno) if (result && result.data) { const ownerSocialSecurityNumber = result.data.ownerSocialSecurityNumber @@ -159,7 +48,7 @@ export class IcelandicTransportAuthorityServices { await this.vehicleService.create(vehicle) } - return true + return result.data } logger.error( diff --git a/apps/skilavottord/ws/src/app/utils/const.ts b/apps/skilavottord/ws/src/app/utils/const.ts new file mode 100644 index 000000000000..b4b53f7da574 --- /dev/null +++ b/apps/skilavottord/ws/src/app/utils/const.ts @@ -0,0 +1,5 @@ +/** Samgöngustofa API versions */ +export enum ApiVersion { + REGISTRATIONS = '3.0', + INFORMATION = '1.0', +} diff --git a/apps/skilavottord/ws/src/app/utils/index.ts b/apps/skilavottord/ws/src/app/utils/index.ts new file mode 100644 index 000000000000..05d610c5e491 --- /dev/null +++ b/apps/skilavottord/ws/src/app/utils/index.ts @@ -0,0 +1,2 @@ +export * from './const' +export * from './skilavottordUtils' diff --git a/apps/skilavottord/ws/src/app/utils/skilavottordUtils.ts b/apps/skilavottord/ws/src/app/utils/skilavottordUtils.ts new file mode 100644 index 000000000000..77120b29d8e4 --- /dev/null +++ b/apps/skilavottord/ws/src/app/utils/skilavottordUtils.ts @@ -0,0 +1,8 @@ +/** + * Shorten the permno for logging purpose + * @param permno + * @returns + */ +export const getShortPermno = (permno: string) => { + return permno.slice(-3) +} diff --git a/apps/web/components/Organization/Slice/TeamListSlice/TeamListSlice.tsx b/apps/web/components/Organization/Slice/TeamListSlice/TeamListSlice.tsx index cfaaf02018c9..c6b06f0a76a6 100644 --- a/apps/web/components/Organization/Slice/TeamListSlice/TeamListSlice.tsx +++ b/apps/web/components/Organization/Slice/TeamListSlice/TeamListSlice.tsx @@ -1,8 +1,9 @@ -import { useState } from 'react' +import { useMemo, useState } from 'react' import flatten from 'lodash/flatten' import { useLazyQuery } from '@apollo/client' import { TeamList, type TeamListProps } from '@island.is/island-ui/contentful' +import { sortAlpha } from '@island.is/shared/utils' import { GenericList } from '@island.is/web/components' import { type GenericTag, @@ -65,7 +66,44 @@ export const TeamMemberListWrapper = ({ ) const totalItems = itemsResponse?.total ?? 0 - const items = itemsResponse?.items ?? [] + + const items = useMemo( + () => + (itemsResponse?.items ?? []).map((item) => { + const tagGroups: { groupLabel: string; tagLabels: string[] }[] = [] + for (const tag of item.filterTags ?? []) { + if (!tag.genericTagGroup?.title || !tag.title) { + continue + } + const index = tagGroups.findIndex( + (group) => group.groupLabel === tag.genericTagGroup?.title, + ) + if (index >= 0) { + tagGroups[index].tagLabels.push(tag.title) + } else { + tagGroups.push({ + groupLabel: tag.genericTagGroup.title, + tagLabels: [tag.title], + }) + } + + // Add a colon to the end of group labels if it doesn't have one + for (const group of tagGroups) { + if (!group.groupLabel.endsWith(':')) { + group.groupLabel += ':' + } + } + } + + tagGroups.sort(sortAlpha('groupLabel')) + + return { + ...(item as TeamListProps['teamMembers'][number]), + tagGroups, + } + }), + [itemsResponse], + ) return ( ) diff --git a/apps/web/infra/web.ts b/apps/web/infra/web.ts index 9ce9bf73fd4f..fd73c10031e9 100644 --- a/apps/web/infra/web.ts +++ b/apps/web/infra/web.ts @@ -54,8 +54,8 @@ export const serviceSetup = (services: { .liveness('/liveness') .readiness({ path: '/readiness', initialDelaySeconds: 20 }) .resources({ - limits: { cpu: '400m', memory: '768Mi' }, - requests: { cpu: '200m', memory: '384Mi' }, + limits: { cpu: '1000m', memory: '768Mi' }, + requests: { cpu: '300m', memory: '384Mi' }, }) .replicaCount({ default: 2, diff --git a/apps/web/public/.well-known/apple-app-site-association b/apps/web/public/.well-known/apple-app-site-association index aee27712cdc5..fd6c7aeccefd 100644 --- a/apps/web/public/.well-known/apple-app-site-association +++ b/apps/web/public/.well-known/apple-app-site-association @@ -1,5 +1,24 @@ { - "applinks": {}, + "applinks": { + "details": [ + { + "appIDs": [ + "J3WWZR9JLF.is.island.app", + "J3WWZR9JLF.is.island.app.dev" + ], + "components": [ + { "/": "/minarsidur/postholf" }, + { "/": "/minarsidur/postholf/*" }, + { "/": "/minarsidur/skirteini" }, + { "/": "/minarsidur/eignir/fasteignir" }, + { "/": "/minarsidur/eignir/fasteignir/*" }, + { "/": "/minarsidur/eignir/okutaeki/min-okutaeki" }, + { "/": "/minarsidur/eignir/okutaeki/min-okutaeki/*" }, + { "/": "/minarsidur/loftbru" } + ] + } + ] + }, "webcredentials": { "apps": [ "J3WWZR9JLF.is.island.app", diff --git a/apps/web/screens/queries/TeamList.ts b/apps/web/screens/queries/TeamList.ts index 99aebeaa1702..b76eb3fb0946 100644 --- a/apps/web/screens/queries/TeamList.ts +++ b/apps/web/screens/queries/TeamList.ts @@ -14,6 +14,18 @@ export const GET_TEAM_MEMBERS_QUERY = gql` items { name title + email + phone + filterTags { + id + title + slug + genericTagGroup { + id + title + slug + } + } image { ...ImageFields } diff --git a/charts/identity-server/values.dev.yaml b/charts/identity-server/values.dev.yaml index 1de58ff732d4..9494dd9708b6 100644 --- a/charts/identity-server/values.dev.yaml +++ b/charts/identity-server/values.dev.yaml @@ -213,6 +213,8 @@ namespaces: services-auth-admin-api: enabled: true env: + COMPANY_REGISTRY_REDIS_NODES: '["clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379"]' + COMPANY_REGISTRY_XROAD_PROVIDER_ID: 'IS-DEV/GOV/10006/Skatturinn/ft-v1' DB_HOST: 'postgres-applications.internal' DB_NAME: 'servicesauth' DB_REPLICAS_HOST: 'postgres-applications-reader.internal' @@ -222,6 +224,11 @@ services-auth-admin-api: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=691 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' + XROAD_NATIONAL_REGISTRY_ACTOR_TOKEN: 'true' + XROAD_NATIONAL_REGISTRY_REDIS_NODES: '["clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379"]' + XROAD_NATIONAL_REGISTRY_SERVICE_PATH: 'IS-DEV/GOV/10001/SKRA-Protected/Einstaklingar-v1' + XROAD_RSK_PROCURING_ACTOR_TOKEN: 'true' + XROAD_RSK_PROCURING_REDIS_NODES: '["clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379"]' grantNamespaces: - 'nginx-ingress-external' - 'nginx-ingress-internal' @@ -275,6 +282,8 @@ services-auth-admin-api: CLIENT_SECRET_ENCRYPTION_KEY: '/k8s/services-auth/admin-api/CLIENT_SECRET_ENCRYPTION_KEY' CONFIGCAT_SDK_KEY: '/k8s/configcat/CONFIGCAT_SDK_KEY' DB_PASS: '/k8s/servicesauth/DB_PASSWORD' + IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' + NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' securityContext: allowPrivilegeEscalation: false privileged: false diff --git a/charts/identity-server/values.prod.yaml b/charts/identity-server/values.prod.yaml index d05db0f9e15c..e6cce69d8c6a 100644 --- a/charts/identity-server/values.prod.yaml +++ b/charts/identity-server/values.prod.yaml @@ -210,6 +210,8 @@ namespaces: services-auth-admin-api: enabled: true env: + COMPANY_REGISTRY_REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]' + COMPANY_REGISTRY_XROAD_PROVIDER_ID: 'IS/GOV/5402696029/Skatturinn/ft-v1' DB_HOST: 'postgres-ids.internal' DB_NAME: 'servicesauth' DB_REPLICAS_HOST: 'postgres-ids.internal' @@ -219,6 +221,11 @@ services-auth-admin-api: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=691 -r dd-trace/init' SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms' + XROAD_NATIONAL_REGISTRY_ACTOR_TOKEN: 'true' + XROAD_NATIONAL_REGISTRY_REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]' + XROAD_NATIONAL_REGISTRY_SERVICE_PATH: 'IS/GOV/6503760649/SKRA-Protected/Einstaklingar-v1' + XROAD_RSK_PROCURING_ACTOR_TOKEN: 'true' + XROAD_RSK_PROCURING_REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]' grantNamespaces: - 'nginx-ingress-external' - 'nginx-ingress-internal' @@ -272,6 +279,8 @@ services-auth-admin-api: CLIENT_SECRET_ENCRYPTION_KEY: '/k8s/services-auth/admin-api/CLIENT_SECRET_ENCRYPTION_KEY' CONFIGCAT_SDK_KEY: '/k8s/configcat/CONFIGCAT_SDK_KEY' DB_PASS: '/k8s/servicesauth/DB_PASSWORD' + IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' + NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' securityContext: allowPrivilegeEscalation: false privileged: false diff --git a/charts/identity-server/values.staging.yaml b/charts/identity-server/values.staging.yaml index e8b29ca0e315..9bca8e248e64 100644 --- a/charts/identity-server/values.staging.yaml +++ b/charts/identity-server/values.staging.yaml @@ -213,6 +213,8 @@ namespaces: services-auth-admin-api: enabled: true env: + COMPANY_REGISTRY_REDIS_NODES: '["clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379"]' + COMPANY_REGISTRY_XROAD_PROVIDER_ID: 'IS-TEST/GOV/5402696029/Skatturinn/ft-v1' DB_HOST: 'postgres-applications.internal' DB_NAME: 'servicesauth' DB_REPLICAS_HOST: 'postgres-applications.internal' @@ -222,6 +224,11 @@ services-auth-admin-api: LOG_LEVEL: 'info' NODE_OPTIONS: '--max-old-space-size=691 -r dd-trace/init' SERVERSIDE_FEATURES_ON: '' + XROAD_NATIONAL_REGISTRY_ACTOR_TOKEN: 'true' + XROAD_NATIONAL_REGISTRY_REDIS_NODES: '["clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379"]' + XROAD_NATIONAL_REGISTRY_SERVICE_PATH: 'IS-TEST/GOV/6503760649/SKRA-Protected/Einstaklingar-v1' + XROAD_RSK_PROCURING_ACTOR_TOKEN: 'true' + XROAD_RSK_PROCURING_REDIS_NODES: '["clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379"]' grantNamespaces: - 'nginx-ingress-external' - 'nginx-ingress-internal' @@ -275,6 +282,8 @@ services-auth-admin-api: CLIENT_SECRET_ENCRYPTION_KEY: '/k8s/services-auth/admin-api/CLIENT_SECRET_ENCRYPTION_KEY' CONFIGCAT_SDK_KEY: '/k8s/configcat/CONFIGCAT_SDK_KEY' DB_PASS: '/k8s/servicesauth/DB_PASSWORD' + IDENTITY_SERVER_CLIENT_SECRET: '/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET' + NATIONAL_REGISTRY_IDS_CLIENT_SECRET: '/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET' securityContext: allowPrivilegeEscalation: false privileged: false diff --git a/charts/islandis/values.dev.yaml b/charts/islandis/values.dev.yaml index afa723c26f82..910070f734b9 100644 --- a/charts/islandis/values.dev.yaml +++ b/charts/islandis/values.dev.yaml @@ -882,7 +882,7 @@ application-system-api-worker: cpu: '400m' memory: '768Mi' requests: - cpu: '100m' + cpu: '150m' memory: '384Mi' schedule: '*/30 * * * *' secrets: @@ -1735,7 +1735,7 @@ license-api: cpu: '400m' memory: '512Mi' requests: - cpu: '15m' + cpu: '50m' memory: '256Mi' secrets: ADR_LICENSE_PASS_TEMPLATE_ID: '/k8s/api/ADR_LICENSE_PASS_TEMPLATE_ID' @@ -3326,10 +3326,10 @@ web: min: 2 resources: limits: - cpu: '400m' + cpu: '1000m' memory: '768Mi' requests: - cpu: '200m' + cpu: '300m' memory: '384Mi' secrets: CONFIGCAT_SDK_KEY: '/k8s/configcat/CONFIGCAT_SDK_KEY' diff --git a/charts/islandis/values.prod.yaml b/charts/islandis/values.prod.yaml index 9421ae497461..3a23969c8f10 100644 --- a/charts/islandis/values.prod.yaml +++ b/charts/islandis/values.prod.yaml @@ -872,7 +872,7 @@ application-system-api-worker: cpu: '400m' memory: '768Mi' requests: - cpu: '100m' + cpu: '150m' memory: '384Mi' schedule: '*/30 * * * *' secrets: @@ -1602,7 +1602,7 @@ license-api: cpu: '400m' memory: '512Mi' requests: - cpu: '15m' + cpu: '50m' memory: '256Mi' secrets: ADR_LICENSE_PASS_TEMPLATE_ID: '/k8s/api/ADR_LICENSE_PASS_TEMPLATE_ID' @@ -3207,10 +3207,10 @@ web: min: 2 resources: limits: - cpu: '400m' + cpu: '1000m' memory: '768Mi' requests: - cpu: '200m' + cpu: '300m' memory: '384Mi' secrets: CONFIGCAT_SDK_KEY: '/k8s/configcat/CONFIGCAT_SDK_KEY' diff --git a/charts/islandis/values.staging.yaml b/charts/islandis/values.staging.yaml index 4cf11cde3c61..586ed48cbd17 100644 --- a/charts/islandis/values.staging.yaml +++ b/charts/islandis/values.staging.yaml @@ -880,7 +880,7 @@ application-system-api-worker: cpu: '400m' memory: '768Mi' requests: - cpu: '100m' + cpu: '150m' memory: '384Mi' schedule: '*/30 * * * *' secrets: @@ -1480,7 +1480,7 @@ license-api: cpu: '400m' memory: '512Mi' requests: - cpu: '15m' + cpu: '50m' memory: '256Mi' secrets: ADR_LICENSE_PASS_TEMPLATE_ID: '/k8s/api/ADR_LICENSE_PASS_TEMPLATE_ID' @@ -3070,10 +3070,10 @@ web: min: 2 resources: limits: - cpu: '400m' + cpu: '1000m' memory: '768Mi' requests: - cpu: '200m' + cpu: '300m' memory: '384Mi' secrets: CONFIGCAT_SDK_KEY: '/k8s/configcat/CONFIGCAT_SDK_KEY' diff --git a/libs/api/domains/auth-admin/src/lib/auth-admin.module.ts b/libs/api/domains/auth-admin/src/lib/auth-admin.module.ts index 9fd435143145..b130bdace9e6 100644 --- a/libs/api/domains/auth-admin/src/lib/auth-admin.module.ts +++ b/libs/api/domains/auth-admin/src/lib/auth-admin.module.ts @@ -13,10 +13,13 @@ import { ClientSecretLoader } from './client/client-secret.loader' import { ScopeResolver } from './scope/scope.resolver' import { ScopeService } from './scope/scope.service' import { DelegationProviderService } from './delegationProvider/delegation-provider.service' -import { DelegationProviderResolverResolver } from './delegationProvider/delegation-provider.resolver' +import { DelegationProviderResolver } from './delegationProvider/delegation-provider.resolver' +import { DelegationAdminResolver } from './delegationAdmin/delegation-admin.resolver' +import { DelegationAdminService } from './delegationAdmin/delegation-admin.service' +import { AuthDelegationApiClientModule } from '@island.is/clients/auth/delegation-api' @Module({ - imports: [AuthAdminApiClientModule], + imports: [AuthAdminApiClientModule, AuthDelegationApiClientModule], providers: [ TenantResolver, TenantEnvironmentResolver, @@ -29,7 +32,9 @@ import { DelegationProviderResolverResolver } from './delegationProvider/delegat ScopeResolver, ScopeService, DelegationProviderService, - DelegationProviderResolverResolver, + DelegationProviderResolver, + DelegationAdminResolver, + DelegationAdminService, ], }) export class AuthAdminModule {} diff --git a/libs/api/domains/auth-admin/src/lib/delegationAdmin/delegation-admin.resolver.ts b/libs/api/domains/auth-admin/src/lib/delegationAdmin/delegation-admin.resolver.ts new file mode 100644 index 000000000000..56c8aebe59a1 --- /dev/null +++ b/libs/api/domains/auth-admin/src/lib/delegationAdmin/delegation-admin.resolver.ts @@ -0,0 +1,115 @@ +import { + Args, + Mutation, + Parent, + Query, + ResolveField, + Resolver, +} from '@nestjs/graphql' +import { NotFoundException, UseGuards } from '@nestjs/common' + +import { DelegationAdminService } from './delegation-admin.service' +import { DelegationAdminCustomModel } from './models/delegation.model' + +import { + type User, + CurrentUser, + IdsUserGuard, +} from '@island.is/auth-nest-tools' +import { + Identity, + type IdentityDataLoader, + IdentityLoader, +} from '@island.is/api/domains/identity' +import { Loader } from '@island.is/nest/dataloader' +import { DelegationDTO } from '@island.is/auth-api-lib' +import { + type DomainDataLoader, + CustomDelegation, + Domain, + DomainLoader, + ISLAND_DOMAIN, +} from '@island.is/api/domains/auth' + +@UseGuards(IdsUserGuard) +@Resolver(CustomDelegation) +export class DelegationAdminResolver { + constructor( + private readonly delegationAdminService: DelegationAdminService, + ) {} + + @Query(() => DelegationAdminCustomModel, { name: 'authAdminDelegationAdmin' }) + async getDelegationSystem( + @Args('nationalId') nationalId: string, + @CurrentUser() user: User, + @Loader(IdentityLoader) identityLoader: IdentityDataLoader, + ) { + const delegations = await this.delegationAdminService.getDelegationAdmin( + user, + nationalId, + ) + const identityCard = await identityLoader.load(nationalId) + + return { + nationalId: nationalId, + name: identityCard.name, + incoming: delegations.incoming, + outgoing: delegations.outgoing, + } + } + + @Mutation(() => Boolean, { name: 'authDeleteAdminDelegation' }) + async deleteDelegationSystem( + @Args('id') id: string, + @CurrentUser() user: User, + ) { + return this.delegationAdminService.deleteDelegationAdmin(user, id) + } + + @ResolveField('from', () => Identity) + resolveFromIdentity( + @Loader(IdentityLoader) identityLoader: IdentityDataLoader, + @Parent() customDelegation: DelegationDTO, + ) { + return identityLoader.load(customDelegation.fromNationalId) + } + + @ResolveField('to', () => Identity) + resolveToIdentity( + @Loader(IdentityLoader) identityLoader: IdentityDataLoader, + @Parent() customDelegation: DelegationDTO, + ) { + return identityLoader.load(customDelegation.toNationalId) + } + + @ResolveField('validTo', () => Date, { nullable: true }) + resolveValidTo(@Parent() delegation: DelegationDTO): Date | undefined { + if (!delegation.validTo) { + return undefined + } + + return delegation.scopes?.every( + (scope) => scope.validTo?.toString() === delegation.validTo?.toString(), + ) + ? delegation.validTo + : undefined + } + + @ResolveField('domain', () => Domain) + async resolveDomain( + @Loader(DomainLoader) domainLoader: DomainDataLoader, + @Parent() delegation: DelegationDTO, + ): Promise { + const domainName = delegation.domainName ?? ISLAND_DOMAIN + const domain = await domainLoader.load({ + lang: 'is', + domain: domainName, + }) + + if (!domain) { + throw new NotFoundException(`Could not find domain: ${domainName}`) + } + + return domain + } +} diff --git a/libs/api/domains/auth-admin/src/lib/delegationAdmin/delegation-admin.service.ts b/libs/api/domains/auth-admin/src/lib/delegationAdmin/delegation-admin.service.ts new file mode 100644 index 000000000000..7f884ed7325f --- /dev/null +++ b/libs/api/domains/auth-admin/src/lib/delegationAdmin/delegation-admin.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common' + +import { + DelegationAdminApi, + DelegationAdminCustomDto, +} from '@island.is/clients/auth/admin-api' +import { Auth, AuthMiddleware, User } from '@island.is/auth-nest-tools' + +@Injectable() +export class DelegationAdminService { + constructor(private readonly delegationAdminApi: DelegationAdminApi) {} + + delegationsWithAuth(auth: Auth) { + return this.delegationAdminApi.withMiddleware(new AuthMiddleware(auth)) + } + + async getDelegationAdmin( + user: User, + nationalId: string, + ): Promise { + return await this.delegationsWithAuth( + user, + ).delegationAdminControllerGetDelegationAdmin({ + xQueryNationalId: nationalId, + }) + } + + async deleteDelegationAdmin( + user: User, + delegationId: string, + ): Promise { + try { + await this.delegationsWithAuth(user).delegationAdminControllerDelete({ + delegationId, + }) + + return true + } catch { + return false + } + } +} diff --git a/libs/api/domains/auth-admin/src/lib/delegationAdmin/models/delegation.model.ts b/libs/api/domains/auth-admin/src/lib/delegationAdmin/models/delegation.model.ts new file mode 100644 index 000000000000..a97a1367d554 --- /dev/null +++ b/libs/api/domains/auth-admin/src/lib/delegationAdmin/models/delegation.model.ts @@ -0,0 +1,17 @@ +import { Field, ObjectType } from '@nestjs/graphql' +import { CustomDelegation } from '@island.is/api/domains/auth' + +@ObjectType('DelegationAdminCustomModel') +export class DelegationAdminCustomModel { + @Field(() => String, { nullable: false }) + nationalId!: string + + @Field(() => String, { nullable: false }) + name!: string + + @Field(() => [CustomDelegation]) + incoming!: CustomDelegation[] + + @Field(() => [CustomDelegation]) + outgoing!: CustomDelegation[] +} diff --git a/libs/api/domains/auth-admin/src/lib/delegationProvider/delegation-provider.resolver.ts b/libs/api/domains/auth-admin/src/lib/delegationProvider/delegation-provider.resolver.ts index b70c5bbbacb0..6a78581b03b2 100644 --- a/libs/api/domains/auth-admin/src/lib/delegationProvider/delegation-provider.resolver.ts +++ b/libs/api/domains/auth-admin/src/lib/delegationProvider/delegation-provider.resolver.ts @@ -12,7 +12,7 @@ import { DelegationProviderService } from './delegation-provider.service' @UseGuards(IdsUserGuard) @Resolver(() => DelegationProvider) -export class DelegationProviderResolverResolver { +export class DelegationProviderResolver { constructor(private delegationProvider: DelegationProviderService) {} @Query(() => DelegationProviderPayload, { diff --git a/libs/api/domains/auth/src/index.ts b/libs/api/domains/auth/src/index.ts index 781ba4ad68a5..c6bd4b5d90f1 100644 --- a/libs/api/domains/auth/src/index.ts +++ b/libs/api/domains/auth/src/index.ts @@ -3,4 +3,12 @@ export { AuthModule } from './lib/auth.module' export { ClientLoader } from './lib/loaders/client.loader' export type { ClientDataLoader } from './lib/loaders/client.loader' +export { DomainLoader } from './lib/loaders/domain.loader' +export type { DomainDataLoader } from './lib/loaders/domain.loader' + +export { Domain } from './lib/models/domain.model' export { Client } from './lib/models/client.model' +export { DelegationScope } from './lib/models/delegationScope.model' +export { CustomDelegation } from './lib/models/delegation.model' + +export { ISLAND_DOMAIN } from './lib/services/constants' diff --git a/libs/application/api/files/src/lib/files.module.ts b/libs/application/api/files/src/lib/files.module.ts index de597a52fbbe..c02b031a3ad3 100644 --- a/libs/application/api/files/src/lib/files.module.ts +++ b/libs/application/api/files/src/lib/files.module.ts @@ -25,7 +25,7 @@ import { ApplicationApiCoreModule } from '@island.is/application/api/core' }) export class ApplicationFilesModule {} -export function createBullModule() { +export const createBullModule = () => { if (process.env.INIT_SCHEMA === 'true') { return NestBullModule.registerQueueAsync() } else { diff --git a/libs/application/api/files/src/lib/pdfGenerators/residenceChange.ts b/libs/application/api/files/src/lib/pdfGenerators/residenceChange.ts index 9d064c55750f..a580ac0f98c2 100644 --- a/libs/application/api/files/src/lib/pdfGenerators/residenceChange.ts +++ b/libs/application/api/files/src/lib/pdfGenerators/residenceChange.ts @@ -24,7 +24,9 @@ import { addLegalEffect } from './familyMatters' const TEMPORARY = 'temporary' -export async function generateResidenceChangePdf(application: CRCApplication) { +export const generateResidenceChangePdf = async ( + application: CRCApplication, +) => { const { answers, externalData: { nationalRegistry }, diff --git a/libs/application/api/payment/src/lib/types/Charge.ts b/libs/application/api/payment/src/lib/types/Charge.ts index c3b7d9a6b46e..0e2dab15298b 100644 --- a/libs/application/api/payment/src/lib/types/Charge.ts +++ b/libs/application/api/payment/src/lib/types/Charge.ts @@ -2,13 +2,13 @@ import { User } from '@island.is/auth-nest-tools' import { Charge, ExtraData } from '@island.is/clients/charge-fjs-v2' import { Payment } from '../payment.model' -export function formatCharge( +export const formatCharge = ( payment: Payment, callbackBaseUrl: string, callbackAdditionUrl: string, extraData: ExtraData[] | undefined, user: User, -): Charge { +): Charge => { // TODO: island.is x-road service path for callback.. ?? // this can actually be a fixed url const callbackUrl = diff --git a/libs/application/core/README.md b/libs/application/core/README.md index e17d5f5b337c..832dd87a7852 100644 --- a/libs/application/core/README.md +++ b/libs/application/core/README.md @@ -405,9 +405,9 @@ export interface NewField extends BaseField { 4. Create a new function in `libs/application/core/src/lib/fieldBuilders.ts`. This function accepts a parameter of the new type we created, `NewField`, but we have to omit `type`, `component` and `children`. Then add the props as follows. ```typescript -export function buildNewField( +export const buildNewField = ( data: Omit, -): NewField { +): NewField => { const { myProp, myOtherProp } = data return { ...extractCommonFields(data), diff --git a/libs/application/core/src/lib/conditionUtils.ts b/libs/application/core/src/lib/conditionUtils.ts index f8b26d4c0440..a326cff89b6a 100644 --- a/libs/application/core/src/lib/conditionUtils.ts +++ b/libs/application/core/src/lib/conditionUtils.ts @@ -11,12 +11,12 @@ import { import { getValueViaPath } from './formUtils' import { User } from '@island.is/shared/types' -function applyStaticConditionalCheck( +const applyStaticConditionalCheck = ( formValue: FormValue, externalData: ExternalData, check: StaticCheck, user: User | null, -): boolean { +): boolean => { const { value, questionId, comparator, externalDataId, userPropId } = check let isValid = false let valueViaPath @@ -68,12 +68,12 @@ function applyStaticConditionalCheck( return isValid } -export function shouldShowFormItem( +export const shouldShowFormItem = ( formItem: FormItem, formValue: FormValue, externalData: ExternalData = {}, user: User | null, -): boolean { +): boolean => { const { condition } = formItem if (!condition) { return true diff --git a/libs/application/core/src/lib/fieldBuilders.ts b/libs/application/core/src/lib/fieldBuilders.ts index 049cba433e2f..fef014754762 100644 --- a/libs/application/core/src/lib/fieldBuilders.ts +++ b/libs/application/core/src/lib/fieldBuilders.ts @@ -82,9 +82,9 @@ const extractCommonFields = ( } } -export function buildCheckboxField( +export const buildCheckboxField = ( data: Omit, -): CheckboxField { +): CheckboxField => { const { options, strong = false, @@ -107,9 +107,9 @@ export function buildCheckboxField( } } -export function buildDateField( +export const buildDateField = ( data: Omit, -): DateField { +): DateField => { const { maxDate, minDate, @@ -134,9 +134,9 @@ export function buildDateField( } } -export function buildDescriptionField( +export const buildDescriptionField = ( data: Omit, -): DescriptionField { +): DescriptionField => { const { titleVariant = 'h2', description, @@ -163,9 +163,9 @@ export function buildDescriptionField( } } -export function buildRadioField( +export const buildRadioField = ( data: Omit, -): RadioField { +): RadioField => { const { options, largeButtons = true, @@ -191,9 +191,9 @@ export function buildRadioField( } } -export function buildSelectField( +export const buildSelectField = ( data: Omit, -): SelectField { +): SelectField => { const { options, placeholder, @@ -216,9 +216,9 @@ export function buildSelectField( } } -export function buildAsyncSelectField( +export const buildAsyncSelectField = ( data: Omit, -): AsyncSelectField { +): AsyncSelectField => { const { loadOptions, loadingError, @@ -244,9 +244,9 @@ export function buildAsyncSelectField( } } -export function buildCompanySearchField( +export const buildCompanySearchField = ( data: Omit, -): CompanySearchField { +): CompanySearchField => { const { placeholder, shouldIncludeIsatNumber, @@ -266,9 +266,9 @@ export function buildCompanySearchField( } } -export function buildTextField( +export const buildTextField = ( data: Omit, -): TextField { +): TextField => { const { backgroundColor = 'blue', placeholder, @@ -305,9 +305,9 @@ export function buildTextField( } } -export function buildPhoneField( +export const buildPhoneField = ( data: Omit, -): PhoneField { +): PhoneField => { const { backgroundColor = 'blue', placeholder, @@ -332,10 +332,10 @@ export function buildPhoneField( } } -export function buildCustomField( +export const buildCustomField = ( data: Omit, props?: RecordObject, -): CustomField { +): CustomField => { const { component, childInputIds } = data return { ...extractCommonFields(data), @@ -347,9 +347,9 @@ export function buildCustomField( } } -export function buildFileUploadField( +export const buildFileUploadField = ( data: Omit, -): FileUploadField { +): FileUploadField => { const { introduction, uploadHeader, @@ -385,11 +385,11 @@ export function buildFileUploadField( } } -export function buildDividerField(data: { +export const buildDividerField = (data: { condition?: Condition title?: FormText color?: Colors -}): DividerField { +}): DividerField => { const { title, color, condition } = data return { id: '', @@ -403,7 +403,7 @@ export function buildDividerField(data: { } } -export function buildKeyValueField(data: { +export const buildKeyValueField = (data: { label: FormText value: FormText | FormTextArray width?: FieldWidth @@ -414,7 +414,7 @@ export function buildKeyValueField(data: { paddingX?: BoxProps['padding'] paddingY?: BoxProps['padding'] paddingBottom?: BoxProps['padding'] -}): KeyValueField { +}): KeyValueField => { const { label, value, @@ -448,13 +448,13 @@ export function buildKeyValueField(data: { } } -export function buildSubmitField(data: { +export const buildSubmitField = (data: { id: string title: FormText placement?: 'footer' | 'screen' refetchApplicationAfterSubmit?: boolean actions: CallToAction[] -}): SubmitField { +}): SubmitField => { const { id, placement = 'footer', @@ -478,11 +478,11 @@ export function buildSubmitField(data: { } } -export function buildFieldOptions( +export const buildFieldOptions = ( maybeOptions: MaybeWithApplicationAndField, application: Application, field: Field, -): Option[] { +): Option[] => { if (typeof maybeOptions === 'function') { return maybeOptions(application, field) } @@ -490,10 +490,10 @@ export function buildFieldOptions( return maybeOptions } -export function buildRedirectToServicePortalField(data: { +export const buildRedirectToServicePortalField = (data: { id: string title: FormText -}): RedirectToServicePortalField { +}): RedirectToServicePortalField => { const { id, title } = data return { children: undefined, @@ -504,10 +504,10 @@ export function buildRedirectToServicePortalField(data: { } } -export function buildPaymentPendingField(data: { +export const buildPaymentPendingField = (data: { id: string title: FormText -}): PaymentPendingField { +}): PaymentPendingField => { const { id, title } = data return { children: undefined, @@ -518,9 +518,9 @@ export function buildPaymentPendingField(data: { } } -export function buildMessageWithLinkButtonField( +export const buildMessageWithLinkButtonField = ( data: Omit, -): MessageWithLinkButtonField { +): MessageWithLinkButtonField => { const { id, title, url, message, buttonTitle, marginBottom, marginTop } = data return { children: undefined, @@ -536,9 +536,9 @@ export function buildMessageWithLinkButtonField( } } -export function buildExpandableDescriptionField( +export const buildExpandableDescriptionField = ( data: Omit, -): ExpandableDescriptionField { +): ExpandableDescriptionField => { const { id, title, description, introText, startExpanded } = data return { children: undefined, @@ -551,9 +551,9 @@ export function buildExpandableDescriptionField( component: FieldComponents.EXPANDABLE_DESCRIPTION, } } -export function buildAlertMessageField( +export const buildAlertMessageField = ( data: Omit, -): AlertMessageField { +): AlertMessageField => { const { message, alertType, marginTop, marginBottom, links } = data return { ...extractCommonFields(data), @@ -568,9 +568,9 @@ export function buildAlertMessageField( } } -export function buildLinkField( +export const buildLinkField = ( data: Omit, -): LinkField { +): LinkField => { const { s3key, link, iconProps } = data return { ...extractCommonFields(data), @@ -583,9 +583,9 @@ export function buildLinkField( } } -export function buildPaymentChargeOverviewField( +export const buildPaymentChargeOverviewField = ( data: Omit, -): PaymentChargeOverviewField { +): PaymentChargeOverviewField => { const { id, title, forPaymentLabel, totalLabel, getSelectedChargeItems } = data return { @@ -600,9 +600,9 @@ export function buildPaymentChargeOverviewField( } } -export function buildImageField( +export const buildImageField = ( data: Omit, -): ImageField { +): ImageField => { const { id, title, @@ -633,9 +633,9 @@ export function buildImageField( } } -export function buildPdfLinkButtonField( +export const buildPdfLinkButtonField = ( data: Omit, -): PdfLinkButtonField { +): PdfLinkButtonField => { const { verificationDescription, verificationLinkTitle, @@ -714,9 +714,9 @@ export const buildHiddenInput = ( } } -export function buildNationalIdWithNameField( +export const buildNationalIdWithNameField = ( data: Omit, -): NationalIdWithNameField { +): NationalIdWithNameField => { const { disabled, required, @@ -747,9 +747,9 @@ export function buildNationalIdWithNameField( } } -export function buildActionCardListField( +export const buildActionCardListField = ( data: Omit, -): ActionCardListField { +): ActionCardListField => { const { items, space, marginTop, marginBottom } = data return { @@ -764,9 +764,9 @@ export function buildActionCardListField( } } -export function buildTableRepeaterField( +export const buildTableRepeaterField = ( data: Omit, -): TableRepeaterField { +): TableRepeaterField => { const { fields, table, @@ -804,7 +804,7 @@ export function buildTableRepeaterField( } } -export function buildStaticTableField( +export const buildStaticTableField = ( data: Omit< StaticTableField, | 'type' @@ -817,7 +817,7 @@ export function buildStaticTableField( | 'disabled' | 'width' >, -): StaticTableField { +): StaticTableField => { const { header, condition, diff --git a/libs/application/core/src/lib/formBuilders.ts b/libs/application/core/src/lib/formBuilders.ts index 65360368d31d..1334684288a3 100644 --- a/libs/application/core/src/lib/formBuilders.ts +++ b/libs/application/core/src/lib/formBuilders.ts @@ -11,29 +11,29 @@ import { DataProviderBuilderItem, } from '@island.is/application/types' -export function buildForm(data: Omit): Form { +export const buildForm = (data: Omit): Form => { return { ...data, type: FormItemTypes.FORM } } -export function buildMultiField(data: Omit): MultiField { +export const buildMultiField = (data: Omit): MultiField => { return { ...data, type: FormItemTypes.MULTI_FIELD } } -export function buildRepeater(data: Omit): Repeater { +export const buildRepeater = (data: Omit): Repeater => { return { ...data, type: FormItemTypes.REPEATER } } -export function buildSection(data: Omit): Section { +export const buildSection = (data: Omit): Section => { return { ...data, type: FormItemTypes.SECTION } } -export function buildSubSection(data: Omit): SubSection { +export const buildSubSection = (data: Omit): SubSection => { return { ...data, type: FormItemTypes.SUB_SECTION } } -export function buildExternalDataProvider( +export const buildExternalDataProvider = ( data: Omit, -): ExternalDataProvider { +): ExternalDataProvider => { return { ...data, isPartOfRepeater: false, @@ -42,9 +42,9 @@ export function buildExternalDataProvider( } } -export function buildDataProviderItem( +export const buildDataProviderItem = ( data: DataProviderBuilderItem, -): DataProviderItem { +): DataProviderItem => { return { id: data.provider?.externalDataId ?? data.provider?.action ?? '', action: data.provider?.actionId, @@ -56,8 +56,8 @@ export function buildDataProviderItem( } } -export function buildDataProviderPermissionItem( +export const buildDataProviderPermissionItem = ( data: DataProviderPermissionItem, -): DataProviderPermissionItem { +): DataProviderPermissionItem => { return data } diff --git a/libs/application/core/src/lib/formUtils.ts b/libs/application/core/src/lib/formUtils.ts index 6055233140ce..f4586e39971d 100644 --- a/libs/application/core/src/lib/formUtils.ts +++ b/libs/application/core/src/lib/formUtils.ts @@ -39,15 +39,15 @@ const containsArray = (obj: RecordObject) => { return contains } -export function getErrorViaPath(obj: RecordObject, path: string): string { +export const getErrorViaPath = (obj: RecordObject, path: string): string => { return get(obj, path) as string } -export function getValueViaPath( +export const getValueViaPath = ( obj: RecordObject, path: string, defaultValue?: T, -): T | undefined { +): T | undefined => { // Errors from dataSchema with array of object looks like e.g. `{ 'periods[1].startDate': 'error message' }` if (path.match(/.\[\d\]\../g) && !containsArray(obj)) { return (obj?.[path] ?? defaultValue) as T @@ -111,12 +111,12 @@ export const getFormNodeLeaves = (node: FormNode): FormLeaf[] => { return leaves } -export function getSectionsInForm( +export const getSectionsInForm = ( form: Form, answers: FormValue, externalData: ExternalData, user: User | null, -): Section[] { +): Section[] => { const sections: Section[] = [] form.children.forEach((child) => { const shouldShowSection = shouldShowFormItem( @@ -132,12 +132,12 @@ export function getSectionsInForm( return sections } -export function getSubSectionsInSection( +export const getSubSectionsInSection = ( section: Section, answers: FormValue, externalData: ExternalData, user: User | null, -): SubSection[] { +): SubSection[] => { const subSections: SubSection[] = [] section?.children.forEach((child) => { const shouldShowSection = shouldShowFormItem( @@ -153,10 +153,10 @@ export function getSubSectionsInSection( return subSections } -export function findSectionIndex( +export const findSectionIndex = ( sections: Section[], section: Section, -): number { +): number => { if (!sections.length) { return -1 } @@ -168,10 +168,10 @@ export function findSectionIndex( return -1 } -export function findSubSectionIndex( +export const findSubSectionIndex = ( subSections: SubSection[], subSection: SubSection, -): number { +): number => { for (let i = 0; i < subSections.length; i++) { if (subSections[i].id === subSection.id) { return i @@ -215,10 +215,10 @@ const overwriteArrayMerge = ( return destination } -export function mergeAnswers( +export const mergeAnswers = ( currentAnswers: RecordObject, newAnswers: RecordObject, -): FormValue { +): FormValue => { return deepmerge(currentAnswers, newAnswers, { arrayMerge: overwriteArrayMerge, }) @@ -242,11 +242,11 @@ const handleMessageFormatting = ( return formatMessage(descriptor, values) } -export function formatText( +export const formatText = ( text: T, application: Application, formatMessage: MessageFormatter, -): T extends FormTextArray ? string[] : string { +): T extends FormTextArray ? string[] : string => { if (typeof text === 'function') { const message = (text as (_: Application) => StaticText | StaticText[])( application, @@ -278,18 +278,18 @@ export function formatText( return formatMessage(text) as T extends FormTextArray ? string[] : string } -export function formatAndParseAsHTML( +export const formatAndParseAsHTML = ( text: FormText, application: Application, formatMessage: MessageFormatter, -) { +) => { return HtmlParser(formatText(text, application, formatMessage)) } // periods[3].startDate -> 3 // notPartOfRepeater -> -1 // periods[5ab33f1].id -> -1 -export function extractRepeaterIndexFromField(field: Field): number { +export const extractRepeaterIndexFromField = (field: Field): number => { if (!field?.isPartOfRepeater) { return -1 } diff --git a/libs/application/core/src/validation/validators.ts b/libs/application/core/src/validation/validators.ts index 1956f6225e50..8d347ccd7766 100644 --- a/libs/application/core/src/validation/validators.ts +++ b/libs/application/core/src/validation/validators.ts @@ -14,11 +14,11 @@ import { import { coreErrorMessages } from '../lib/messages' import { AnswerValidationError } from './AnswerValidator' -function populateError( +const populateError = ( error: ZodIssue[], pathToError: string | undefined, formatMessage: FormatMessage, -) { +) => { let errorObject = {} error.forEach((element) => { const defaultZodError = element.message === 'Invalid input' @@ -39,7 +39,7 @@ function populateError( return errorObject } -export function validateAnswers({ +export const validateAnswers = ({ dataSchema, answers, formatMessage, @@ -48,7 +48,7 @@ export function validateAnswers({ answers: FormValue isFullSchemaValidation?: boolean formatMessage: FormatMessage -}): ValidationRecord | undefined { +}): ValidationRecord | undefined => { try { if (dataSchema instanceof ZodEffects) { // cases where zod schema has a refinement on the schema object, needs to be defined partial diff --git a/libs/application/template-api-modules/src/lib/modules/templates/children-residence-change-v2/pdfGenerators/templates/residenceChange.ts b/libs/application/template-api-modules/src/lib/modules/templates/children-residence-change-v2/pdfGenerators/templates/residenceChange.ts index 2faf8f5ea364..0e5a3b42f410 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/children-residence-change-v2/pdfGenerators/templates/residenceChange.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/children-residence-change-v2/pdfGenerators/templates/residenceChange.ts @@ -24,7 +24,9 @@ import { DistrictCommissionerLogo } from '../assets/logo' const TEMPORARY = 'temporary' -export async function generateResidenceChangePdf(application: CRCApplication) { +export const generateResidenceChangePdf = async ( + application: CRCApplication, +) => { const { answers, externalData: { nationalRegistry }, diff --git a/libs/application/template-api-modules/src/lib/modules/templates/children-residence-change-v2/utils.ts b/libs/application/template-api-modules/src/lib/modules/templates/children-residence-change-v2/utils.ts index a812e261eda7..ea6d93eb248c 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/children-residence-change-v2/utils.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/children-residence-change-v2/utils.ts @@ -46,7 +46,7 @@ const syslumennOffices = { }, } -export function syslumennDataFromPostalCode(postalCode: string) { +export const syslumennDataFromPostalCode = (postalCode: string) => { for (const [, value] of Object.entries(syslumennOffices)) { if ( value.postalCodePrefixes.some((prefix) => postalCode.startsWith(prefix)) diff --git a/libs/application/template-api-modules/src/lib/modules/templates/complaints-to-althingi-ombudsman/pdfGenerators/templates/complaint.ts b/libs/application/template-api-modules/src/lib/modules/templates/complaints-to-althingi-ombudsman/pdfGenerators/templates/complaint.ts index c306ee10a1b3..9c72d1b97f2f 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/complaints-to-althingi-ombudsman/pdfGenerators/templates/complaint.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/complaints-to-althingi-ombudsman/pdfGenerators/templates/complaint.ts @@ -9,7 +9,7 @@ import { addHeader, addSubheader, addValue, newDocument } from '../pdfUtils' import { format as formatNationalId } from 'kennitala' import { PdfConstants } from '../constants' -export async function generateComplaintPdf(application: Application) { +export const generateComplaintPdf = async (application: Application) => { const answers = application.answers as ComplaintsToAlthingiOmbudsmanAnswers const complainedFor = answers.complainedFor.decision === ComplainedForTypes.MYSELF diff --git a/libs/application/template-api-modules/src/lib/modules/templates/data-protection-complaint/pdfGenerators/pdfGenerator.ts b/libs/application/template-api-modules/src/lib/modules/templates/data-protection-complaint/pdfGenerators/pdfGenerator.ts index e3b9a465793c..36d0ac142e83 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/data-protection-complaint/pdfGenerators/pdfGenerator.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/data-protection-complaint/pdfGenerators/pdfGenerator.ts @@ -3,10 +3,10 @@ import { PdfConstants } from './constants' type generatePdfBody = (template: T, doc: PDFKit.PDFDocument) => void -export async function generatePdf( +export const generatePdf = async ( template: T, generatePdfBody: generatePdfBody, -): Promise { +): Promise => { const doc = new PDFDocument({ margins: { top: PdfConstants.VERTICAL_MARGIN, diff --git a/libs/application/template-api-modules/src/lib/modules/templates/data-protection-complaint/pdfGenerators/templates/complaintPdf.ts b/libs/application/template-api-modules/src/lib/modules/templates/data-protection-complaint/pdfGenerators/templates/complaintPdf.ts index 63eebde8ced8..72e6cd470caa 100644 --- a/libs/application/template-api-modules/src/lib/modules/templates/data-protection-complaint/pdfGenerators/templates/complaintPdf.ts +++ b/libs/application/template-api-modules/src/lib/modules/templates/data-protection-complaint/pdfGenerators/templates/complaintPdf.ts @@ -25,18 +25,18 @@ import parseISO from 'date-fns/parseISO' import is from 'date-fns/locale/is' import { DocumentInfo } from '@island.is/clients/data-protection-complaint' -export async function generateComplaintPdf( +export const generateComplaintPdf = async ( application: Application, attachedFiles: DocumentInfo[], -): Promise { +): Promise => { const dto = applicationToComplaintPDF(application, attachedFiles) return await generatePdf(dto, dpcApplicationPdf) } -function dpcApplicationPdf( +const dpcApplicationPdf = ( complaint: ComplaintPDF, doc: PDFKit.PDFDocument, -): void { +): void => { const timestamp = format( parseISO(complaint.submitDate.toISOString()), 'd. MMMM y', @@ -135,10 +135,10 @@ function dpcApplicationPdf( doc.end() } -function renderExternalDataMessages( +const renderExternalDataMessages = ( externalData: ExternalDataMessages, doc: PDFKit.PDFDocument, -): void { +): void => { addHeader(externalData.title, doc) addValue(externalData.subtitle, doc, PdfConstants.BOLD_FONT) addValue(externalData.description, doc) @@ -155,10 +155,10 @@ function renderExternalDataMessages( doc.moveDown() } -function renderInformationMessages( +const renderInformationMessages = ( information: Information, doc: PDFKit.PDFDocument, -): void { +): void => { addHeader(information.title, doc) const bulletsList = [ @@ -199,10 +199,10 @@ function renderInformationMessages( }) } -function renderContactsAndComplainees( +const renderContactsAndComplainees = ( complaint: ComplaintPDF, doc: PDFKit.PDFDocument, -): void { +): void => { const contactHeading = complaint.onBehalf === OnBehalf.MYSELF || complaint.onBehalf === OnBehalf.ORGANIZATION_OR_INSTITUTION || @@ -291,10 +291,10 @@ function renderContactsAndComplainees( } } -function renderAgencyComplainees( +const renderAgencyComplainees = ( complaint: ComplaintPDF, doc: PDFKit.PDFDocument, -): void { +): void => { if ( complaint.agency?.persons?.length && complaint.onBehalf !== OnBehalf.ORGANIZATION_OR_INSTITUTION && diff --git a/libs/application/template-api-modules/src/lib/modules/templates/health-insurance/bucket/bucket.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/health-insurance/bucket/bucket.service.ts new file mode 100644 index 000000000000..dbad00ed1134 --- /dev/null +++ b/libs/application/template-api-modules/src/lib/modules/templates/health-insurance/bucket/bucket.service.ts @@ -0,0 +1,45 @@ +import { Inject, Injectable } from '@nestjs/common' +import * as AWS from 'aws-sdk' +import * as S3 from 'aws-sdk/clients/s3' +import type { Logger } from '@island.is/logging' +import { LOGGER_PROVIDER } from '@island.is/logging' +import AmazonS3URI from 'amazon-s3-uri' + +@Injectable() +export class BucketService { + private s3 = new AWS.S3({ apiVersion: '2006-03-01' }) + constructor(@Inject(LOGGER_PROVIDER) private logger: Logger) {} + + async getFileContentAsBase64(filename: string): Promise { + this.logger.info('getFileContent base64...') + + const { region, bucket, key } = AmazonS3URI(filename) + const sm = await this.getFile(key, bucket) + if (sm.Body) { + this.logger.info('found file:' + key) + return sm.Body.toString('base64') + } else { + throw new Error('error getting file:' + key) + } + } + + async getFile( + filename: string, + bucketName: string, + ): Promise { + this.logger.info('get bucket file:' + filename) + return new Promise((resolve, reject) => { + const params = { + Bucket: bucketName, + Key: filename, + } + this.s3.getObject(params, (err, data) => { + if (err) { + reject(err) + } else { + resolve(data) + } + }) + }) + } +} diff --git a/libs/application/template-loader/src/index.ts b/libs/application/template-loader/src/index.ts index 995b3bc766fe..7fe9c2f17690 100644 --- a/libs/application/template-loader/src/index.ts +++ b/libs/application/template-loader/src/index.ts @@ -53,14 +53,13 @@ const loadTemplateLib = async ( } } -// eslint-disable-next-line func-style -export async function getApplicationTemplateByTypeId< +export const getApplicationTemplateByTypeId = async < TContext extends ApplicationContext, TStateSchema extends ApplicationStateSchema, TEvents extends EventObject, >( templateId: ApplicationTypes, -): Promise> { +): Promise> => { const templateLib = await loadTemplateLib(templateId) return templateLib.default as ApplicationTemplate< TContext, diff --git a/libs/application/templates/driving-license/src/forms/draft/subSectionDelivery.ts b/libs/application/templates/driving-license/src/forms/draft/subSectionDelivery.ts index a6295a5344c5..0e78b42f4298 100644 --- a/libs/application/templates/driving-license/src/forms/draft/subSectionDelivery.ts +++ b/libs/application/templates/driving-license/src/forms/draft/subSectionDelivery.ts @@ -1,6 +1,8 @@ import { buildDescriptionField, + buildHiddenInputWithWatchedValue, buildMultiField, + buildRadioField, buildSelectField, buildSubSection, } from '@island.is/application/core' @@ -11,6 +13,7 @@ import { } from '../../lib/utils' import { Jurisdiction } from '@island.is/clients/driving-license' +import { B_FULL_RENEWAL_65, Pickup } from '../../lib/constants' export const subSectionDelivery = buildSubSection({ id: 'user', @@ -20,19 +23,17 @@ export const subSectionDelivery = buildSubSection({ buildMultiField({ id: 'info', title: m.pickupLocationTitle, - space: 1, children: [ buildDescriptionField({ - id: 'afhending', - title: m.districtCommisionerTitle, - titleVariant: 'h4', + id: 'jurisdictionHeader', + title: '', description: chooseDistrictCommissionerDescription, }), buildSelectField({ id: 'jurisdiction', - title: m.districtCommisionerPickup, - disabled: false, + title: m.districtCommissionerPickup, required: true, + placeholder: m.districtCommissionerPickupPlaceholder, options: ({ externalData: { jurisdictions: { data }, @@ -45,6 +46,24 @@ export const subSectionDelivery = buildSubSection({ })) }, }), + buildDescriptionField({ + id: 'pickupHeader', + title: '', + description: m.pickupLocationHeader, + titleVariant: 'h4', + space: 'containerGutter', + condition: (answers) => answers.applicationFor === B_FULL_RENEWAL_65, + }), + buildRadioField({ + id: 'pickup', + title: '', + defaultValue: Pickup.POST, + condition: (answers) => answers.applicationFor === B_FULL_RENEWAL_65, + options: [ + { value: Pickup.POST, label: m.overviewPickupPost }, + { value: Pickup.DISTRICT, label: m.overviewPickupDistrict }, + ], + }), ], }), ], diff --git a/libs/application/templates/driving-license/src/forms/draft/subSectionHealthDeclaration.ts b/libs/application/templates/driving-license/src/forms/draft/subSectionHealthDeclaration.ts index d55ada083818..b6c27eee3e1c 100644 --- a/libs/application/templates/driving-license/src/forms/draft/subSectionHealthDeclaration.ts +++ b/libs/application/templates/driving-license/src/forms/draft/subSectionHealthDeclaration.ts @@ -3,19 +3,16 @@ import { buildCustomField, buildSubSection, buildAlertMessageField, - hasYes, buildDescriptionField, YES, } from '@island.is/application/core' -import { NationalRegistryUser } from '@island.is/api/schema' -import { info } from 'kennitala' import { m } from '../../lib/messages' import { hasNoDrivingLicenseInOtherCountry } from '../../lib/utils' import { hasHealthRemarks, needsHealthCertificateCondition, } from '../../lib/utils/formUtils' -import { BE } from '../../lib/constants' +import { BE, B_FULL_RENEWAL_65 } from '../../lib/constants' export const subSectionHealthDeclaration = buildSubSection({ id: 'healthDeclaration', @@ -25,22 +22,15 @@ export const subSectionHealthDeclaration = buildSubSection({ buildMultiField({ id: 'overview', title: m.healthDeclarationMultiFieldTitle, - description: m.healthDeclarationSubTitle, + condition: (answers) => answers.applicationFor !== B_FULL_RENEWAL_65, space: 2, - condition: (answers, externalData) => { - if ((answers.fakeData as any).age) { - return (answers.fakeData as any).age < 65 - } - - return ( - !hasYes(answers?.drivingLicenseInOtherCountry) && - info( - (externalData.nationalRegistry.data as NationalRegistryUser) - .nationalId, - ).age < 65 - ) - }, children: [ + buildDescriptionField({ + id: 'healthDeclarationDescription', + title: '', + description: m.healthDeclarationSubTitle, + marginBottom: 2, + }), buildCustomField({ id: 'remarks', title: '', @@ -173,30 +163,15 @@ export const subSectionHealthDeclaration = buildSubSection({ }), ], }), - /* Different set of the Health Declaration screen for people over the age of 65 */ buildMultiField({ id: 'healthDeclarationAge65', title: m.healthDeclarationMultiFieldTitle, - description: m.healthDeclarationAge65MultiFieldSubTitle, - space: 1, - condition: (answers, externalData) => { - if ((answers.fakeData as any).age) { - return (answers.fakeData as any).age >= 65 - } - - return ( - !hasYes(answers?.drivingLicenseInOtherCountry) && - info( - (externalData.nationalRegistry.data as NationalRegistryUser) - .nationalId, - ).age >= 65 - ) - }, + condition: (answers) => answers.applicationFor === B_FULL_RENEWAL_65, children: [ buildDescriptionField({ - id: 'healthDeclarationAge65Description', + id: 'healthDeclarationDescription65', title: '', - description: 'Þetta view er í vinnslu', + description: m.healthDeclarationMultiField65Description, }), ], }), diff --git a/libs/application/templates/driving-license/src/forms/draft/subSectionQualityPhoto.ts b/libs/application/templates/driving-license/src/forms/draft/subSectionQualityPhoto.ts index 214f3d8ecc0f..0b598d74a8e9 100644 --- a/libs/application/templates/driving-license/src/forms/draft/subSectionQualityPhoto.ts +++ b/libs/application/templates/driving-license/src/forms/draft/subSectionQualityPhoto.ts @@ -5,7 +5,6 @@ import { buildRadioField, buildSubSection, getValueViaPath, - hasYes, buildDescriptionField, } from '@island.is/application/core' import { m } from '../../lib/messages' diff --git a/libs/application/templates/driving-license/src/forms/draft/subSectionSummary.ts b/libs/application/templates/driving-license/src/forms/draft/subSectionSummary.ts index 845d00cde105..8f6485980beb 100644 --- a/libs/application/templates/driving-license/src/forms/draft/subSectionSummary.ts +++ b/libs/application/templates/driving-license/src/forms/draft/subSectionSummary.ts @@ -13,7 +13,7 @@ import { NationalRegistryUser, TeacherV4 } from '../../types/schema' import { m } from '../../lib/messages' import { format as formatKennitala } from 'kennitala' import { StudentAssessment } from '@island.is/api/schema' -import { B_TEMP, BE, YES } from '../../lib/constants' +import { B_FULL_RENEWAL_65, B_TEMP, BE, Pickup, YES } from '../../lib/constants' import { hasNoDrivingLicenseInOtherCountry, isApplicationForCondition, @@ -23,7 +23,7 @@ import { formatPhoneNumber } from '@island.is/application/ui-components' export const subSectionSummary = buildSubSection({ id: 'overview', - title: m.overviewSectionTitle, + title: m.overviewMultiFieldTitle, condition: hasNoDrivingLicenseInOtherCountry, children: [ buildMultiField({ @@ -52,6 +52,8 @@ export const subSectionSummary = buildSubSection({ ? m.applicationForTempLicenseTitle : applicationFor === BE ? m.applicationForBELicenseTitle + : applicationFor === B_FULL_RENEWAL_65 + ? m.applicationForRenewalLicenseTitle : m.applicationForFullLicenseTitle, }), buildDividerField({}), @@ -145,6 +147,16 @@ export const subSectionSummary = buildSubSection({ condition: needsHealthCertificateCondition(YES), }), buildDividerField({}), + buildKeyValueField({ + label: m.pickupLocationTitle, + value: ({ answers }) => { + return answers.pickup === Pickup.POST + ? m.overviewPickupPost + : m.overviewPickupDistrict + }, + width: 'full', + }), + buildDividerField({}), buildKeyValueField({ label: m.overviewPaymentCharge, value: ({ externalData, answers }) => { diff --git a/libs/application/templates/driving-license/src/lib/constants.ts b/libs/application/templates/driving-license/src/lib/constants.ts index 9e10791fd19d..9bab845f6746 100644 --- a/libs/application/templates/driving-license/src/lib/constants.ts +++ b/libs/application/templates/driving-license/src/lib/constants.ts @@ -10,6 +10,11 @@ export const B_TEMP = 'B-temp' export const B_FULL_RENEWAL_65 = 'B-full-renewal-65' export const BE = 'BE' +export enum Pickup { + 'POST' = 'post', + 'DISTRICT' = 'district', +} + export const otherLicenseCategories = ['C', 'C1', 'CE', 'D', 'D1', 'DE'] export const codesRequiringHealthCertificate = ['400', '01.06'] diff --git a/libs/application/templates/driving-license/src/lib/drivingLicenseTemplate.ts b/libs/application/templates/driving-license/src/lib/drivingLicenseTemplate.ts index 5f773c52b519..1f13dbff725b 100644 --- a/libs/application/templates/driving-license/src/lib/drivingLicenseTemplate.ts +++ b/libs/application/templates/driving-license/src/lib/drivingLicenseTemplate.ts @@ -31,6 +31,7 @@ import { BE, B_TEMP, B_FULL, + B_FULL_RENEWAL_65, ApiActions, } from './constants' import { dataSchema } from './dataSchema' @@ -83,6 +84,10 @@ const template: ApplicationTemplate< ? m.applicationForDrivingLicense.defaultMessage + ' - ' + m.applicationForFullLicenseTitle.defaultMessage + : application.answers.applicationFor === B_FULL_RENEWAL_65 + ? m.applicationForDrivingLicense.defaultMessage + + ' - ' + + m.applicationForRenewalLicenseTitle.defaultMessage : m.applicationForDrivingLicense.defaultMessage, institution: m.nationalCommissionerOfPolice, dataSchema, diff --git a/libs/application/templates/driving-license/src/lib/messages.ts b/libs/application/templates/driving-license/src/lib/messages.ts index d487aa4eb58d..41f59615efb6 100644 --- a/libs/application/templates/driving-license/src/lib/messages.ts +++ b/libs/application/templates/driving-license/src/lib/messages.ts @@ -106,10 +106,15 @@ export const m = defineMessages({ description: 'Information', }, pickupLocationTitle: { - id: 'dl.application:pickuplocation', + id: 'dl.application:pickupLocationTitle', defaultMessage: 'Afhendingarstaður', description: 'location for pickup', }, + pickupLocationHeader: { + id: 'dl.application:pickupLocationHeader', + defaultMessage: 'Hvar viltu sækja/fá ökuskírteinið?', + description: 'Where do you want to pick up your driving license?', + }, informationApplicant: { id: 'dl.application:information.applicant', defaultMessage: 'Umsækjandi', @@ -125,6 +130,12 @@ export const m = defineMessages({ defaultMessage: 'Heilbrigðisyfirlýsing', description: 'Health declaration', }, + healthDeclarationMultiField65Description: { + id: 'dl.application:healthDeclarationMultiField65Description#markdown', + defaultMessage: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam auctor, nunc nec ultricies ultricies, nunc nisl ultricies nunc, nec ultricies nunc nisl nec nunc. Nullam auctor, nunc nec ultricies ultricies, nunc nisl ultricies nunc, nec ultricies nunc nisl nec nunc.', + description: 'Health declaration', + }, healthDeclarationMultiFieldSubTitle: { id: 'dl.application:healthDeclarationMultiField.subTitle', defaultMessage: 'Yfirlýsing um líkamlegt og andlegt heilbrigði', @@ -346,6 +357,16 @@ export const m = defineMessages({ defaultMessage: 'Ég kem með vottorð frá lækni meðferðis', description: `I'll bring a certificate from a doctor`, }, + overviewPickupPost: { + id: 'dl.application:overview.pickupPost', + defaultMessage: 'Sent heim í pósti', + description: 'By mail', + }, + overviewPickupDistrict: { + id: 'dl.application:overview.pickupDistrict', + defaultMessage: 'Sækja á afhendingarstað', + description: 'Pickup location', + }, applicationDone: { id: 'dl.application:overview.done', defaultMessage: 'Umsókn móttekin', @@ -604,11 +625,16 @@ export const m = defineMessages({ defaultMessage: 'Sýslumannsembætti', description: 'Title for district commissioner', }, - districtCommisionerPickup: { + districtCommissionerPickup: { id: 'dl.application:districtCommisionerPickup', defaultMessage: 'Afhending', description: 'Pickup for district commissioner', }, + districtCommissionerPickupPlaceholder: { + id: 'dl.application:districtCommisionerPickupPlaceholder', + defaultMessage: 'Veldu sýslumannsembætti', + description: 'Choose district commissioner', + }, chooseDistrictCommisionerForFullLicense: { id: 'dl.application:chooseDistrictCommisionerForFullLicense', defaultMessage: diff --git a/libs/application/templates/family-matters/children-residence-change-v2/src/forms/ContractRejected.ts b/libs/application/templates/family-matters/children-residence-change-v2/src/forms/ContractRejected.ts index eedc28cee6b4..238ec88ac7d4 100644 --- a/libs/application/templates/family-matters/children-residence-change-v2/src/forms/ContractRejected.ts +++ b/libs/application/templates/family-matters/children-residence-change-v2/src/forms/ContractRejected.ts @@ -7,7 +7,7 @@ import { Form, FormModes } from '@island.is/application/types' import Logo from '@island.is/application/templates/family-matters-core/assets/Logo' import * as m from '../lib/messages' -function rejectedForm(id: string): Form { +const rejectedForm = (id: string): Form => { return buildForm({ id: id, title: m.application.name, diff --git a/libs/application/templates/id-card/src/forms/Approved.ts b/libs/application/templates/id-card/src/forms/Approved.ts index 0f6d01423b21..46a6c69b31a7 100644 --- a/libs/application/templates/id-card/src/forms/Approved.ts +++ b/libs/application/templates/id-card/src/forms/Approved.ts @@ -30,7 +30,6 @@ export const Approved: Form = buildForm({ '', ) as string const chosenApplicant = getChosenApplicant( - application.answers, application.externalData, applicantNationalId, ) @@ -54,7 +53,6 @@ export const Approved: Form = buildForm({ '', ) as string const chosenApplicant = getChosenApplicant( - formValue, externalData, applicantNationalId, ) @@ -79,7 +77,6 @@ export const Approved: Form = buildForm({ '', ) as string const chosenApplicant = getChosenApplicant( - formValue, externalData, applicantNationalId, ) diff --git a/libs/application/templates/id-card/src/forms/IdCardForm/IdInformation/ConditionInformation.ts b/libs/application/templates/id-card/src/forms/IdCardForm/IdInformation/ConditionInformation.ts index fc1297f675e7..8a8a5629beec 100644 --- a/libs/application/templates/id-card/src/forms/IdCardForm/IdInformation/ConditionInformation.ts +++ b/libs/application/templates/id-card/src/forms/IdCardForm/IdInformation/ConditionInformation.ts @@ -1,6 +1,7 @@ import { buildSubSection, buildDescriptionField, + getValueViaPath, } from '@island.is/application/core' import { Routes } from '../../../lib/constants' import { idInformation } from '../../../lib/messages/idInformation' @@ -10,7 +11,14 @@ export const ConditionInformationSection = buildSubSection({ id: Routes.CONDITIONINFORMATION, title: idInformation.general.conditionSectionTitle, condition: (formValue, externalData) => { - const chosenApplicant = getChosenApplicant(formValue, externalData) + const radioAnswerApplicant = getValueViaPath( + formValue, + 'chosenApplicants', + ) as string + const chosenApplicant = getChosenApplicant( + externalData, + radioAnswerApplicant, + ) const applicantHasReviewer = hasReviewer(formValue, externalData) return !chosenApplicant.isApplicant && applicantHasReviewer @@ -19,7 +27,20 @@ export const ConditionInformationSection = buildSubSection({ buildDescriptionField({ id: `${Routes.CONDITIONINFORMATION}MultiField`, title: idInformation.general.conditionSectionTitle, - description: idInformation.labels.conditionDescription, + description: (application) => { + const radioAnswerApplicant = getValueViaPath( + application.answers, + 'chosenApplicants', + ) as string + const chosenChild = getChosenApplicant( + application.externalData, + radioAnswerApplicant, + ) + return { + id: idInformation.labels.conditionDescription.id, + values: { parentBName: chosenChild.secondParentName }, + } + }, space: 2, }), ], diff --git a/libs/application/templates/id-card/src/forms/IdCardForm/IdInformation/TypeOfIdSubSection.ts b/libs/application/templates/id-card/src/forms/IdCardForm/IdInformation/TypeOfIdSubSection.ts index d6a0ad0ec889..95e412404524 100644 --- a/libs/application/templates/id-card/src/forms/IdCardForm/IdInformation/TypeOfIdSubSection.ts +++ b/libs/application/templates/id-card/src/forms/IdCardForm/IdInformation/TypeOfIdSubSection.ts @@ -30,9 +30,13 @@ export const TypeOfIdSubSection = buildSubSection({ options: (application) => { const combinedAppplicantInformation = getCombinedApplicantInformation(application.externalData) - const chosenApplicant = getChosenApplicant( + const radioAnswerApplicant = getValueViaPath( application.answers, + 'chosenApplicants', + ) as string + const chosenApplicant = getChosenApplicant( application.externalData, + radioAnswerApplicant, ) if (!chosenApplicant.isApplicant) { @@ -79,7 +83,14 @@ export const TypeOfIdSubSection = buildSubSection({ condition: (answers, externalData) => { const combinedAppplicantInformation = getCombinedApplicantInformation(externalData) - const chosenApplicant = getChosenApplicant(answers, externalData) + const radioAnswerApplicant = getValueViaPath( + answers, + 'chosenApplicants', + ) as string + const chosenApplicant = getChosenApplicant( + externalData, + radioAnswerApplicant, + ) if (!chosenApplicant.isApplicant) { const childPassports = getValueViaPath( externalData, diff --git a/libs/application/templates/id-card/src/forms/Prerequisites/index.ts b/libs/application/templates/id-card/src/forms/Prerequisites/index.ts index f9bfb2f5535a..80e59e4a84c3 100644 --- a/libs/application/templates/id-card/src/forms/Prerequisites/index.ts +++ b/libs/application/templates/id-card/src/forms/Prerequisites/index.ts @@ -106,7 +106,8 @@ export const Prerequisites: Form = buildForm({ }), buildDataProviderItem({ provider: IdentityDocumentApi, - title: '', + title: externalData.identityDocument.title, + subTitle: externalData.identityDocument.subTitle, }), buildDataProviderItem({ provider: SyslumadurPaymentCatalogApi, diff --git a/libs/application/templates/id-card/src/lib/messages/idInformation.ts b/libs/application/templates/id-card/src/lib/messages/idInformation.ts index 021508177acb..e97c4b1d8e33 100644 --- a/libs/application/templates/id-card/src/lib/messages/idInformation.ts +++ b/libs/application/templates/id-card/src/lib/messages/idInformation.ts @@ -86,12 +86,7 @@ export const idInformation = { }, conditionDescription: { id: 'id.application:idInformation.labels.conditionDescription#markdown', - defaultMessage: `Þegar sótt er um fyrir barn þurfa **báðir forsjáraðilar** að - samþykkja umsóknina innan **7 daga**. Viðkomandi fær tilkynningu - um undirritun og getur samþykkt með rafrænum hætti. - \n\nSamþykkið er **vistað rafrænt** hjá Þjóðskrá Íslands. - \n\nSéu rafræn skilríki ekki fyrir hendi er hægt að skila inn - **skriflegu samþykki til Sýslumanns/Þjóðskrár.**`, + defaultMessage: `Þegar sótt er um fyrir barn þurfa báðir forsjáraðilar að samþykkja umsóknina. Þegar þú hefur klárað umsóknina mun {parentBName} fá tilkynningu um að samþykkja þurfi þessa umsókn inni á island.is með rafrænum skilríkjum. {parentBName} hefur 7 daga til að ganga frá samþykkinu og það verður síðan vistað rafrænt hjá Þjóðskrá Íslands. Ef rafræn skilríki eru ekki fyrir hendi er ekki hægt að klára forskráningu hér og sækja verður um á umsóknarstað.`, description: 'Description on condition page', }, }), diff --git a/libs/application/templates/id-card/src/utils/getChosenApplicant.ts b/libs/application/templates/id-card/src/utils/getChosenApplicant.ts index 62a534ddcad3..b70232c00d37 100644 --- a/libs/application/templates/id-card/src/utils/getChosenApplicant.ts +++ b/libs/application/templates/id-card/src/utils/getChosenApplicant.ts @@ -1,20 +1,17 @@ import { getValueViaPath } from '@island.is/application/core' -import { - FormValue, - NationalRegistryIndividual, -} from '@island.is/application/types' +import { NationalRegistryIndividual } from '@island.is/application/types' import { IdentityDocumentChild } from '../lib/constants' export interface ChosenApplicant { name?: string | null isApplicant: boolean - nationalId?: string | null + nationalId: string | null + secondParentName?: string | null } export const getChosenApplicant = ( - answers: FormValue, externalData: any, - nationalId?: string | null, + nationalId: string | null, ): ChosenApplicant => { const applicantIdentity = getValueViaPath( externalData, @@ -28,7 +25,7 @@ export const getChosenApplicant = ( [], ) as Array - if (!nationalId || applicantIdentity?.nationalId === nationalId) { + if (applicantIdentity?.nationalId === nationalId) { return { name: applicantIdentity?.fullName, isApplicant: true, @@ -41,7 +38,8 @@ export const getChosenApplicant = ( return { name: chosenChild.childName, isApplicant: false, - nationalId: chosenChild.childNationalId, + nationalId: chosenChild.childNationalId || '', + secondParentName: chosenChild.secondParentName, } } } diff --git a/libs/application/templates/id-card/src/utils/updateAnswers.ts b/libs/application/templates/id-card/src/utils/updateAnswers.ts index 582681c09b2e..a8dd138072d1 100644 --- a/libs/application/templates/id-card/src/utils/updateAnswers.ts +++ b/libs/application/templates/id-card/src/utils/updateAnswers.ts @@ -9,7 +9,6 @@ export const updateAnswers = ( setValue: (name: string, value: unknown, config?: Object) => void, ): Object => { const chosenApplicants = getChosenApplicant( - application.answers, application.externalData, nationalId, ) diff --git a/libs/application/templates/reference-template/src/fields/ExampleCountryField.tsx b/libs/application/templates/reference-template/src/fields/ExampleCountryField.tsx index bef92882e463..70d0e695371e 100644 --- a/libs/application/templates/reference-template/src/fields/ExampleCountryField.tsx +++ b/libs/application/templates/reference-template/src/fields/ExampleCountryField.tsx @@ -36,7 +36,7 @@ const ExampleCountryField: FC> = ({ // @ts-ignore const [age, setAge] = useState(formValue.person?.age || 0) - function fetchCountries(query = '') { + const fetchCountries = (query = '') => { if (query.length === 0) { return } diff --git a/libs/application/types/src/lib/StateMachine.ts b/libs/application/types/src/lib/StateMachine.ts index fd3c46b8596a..cad79844abe8 100644 --- a/libs/application/types/src/lib/StateMachine.ts +++ b/libs/application/types/src/lib/StateMachine.ts @@ -148,7 +148,7 @@ export type ApplicationStateMachine< > = StateMachine // manually overwrites the initial state for the template as well so the interpreter starts in the current application state -export function createApplicationMachine< +export const createApplicationMachine = < TContext extends ApplicationContext, TStateSchema extends ApplicationStateSchema, TEvent extends EventObject = AnyEventObject, @@ -157,7 +157,7 @@ export function createApplicationMachine< config: StateNodeConfig, options?: Partial>, initialContext?: TContext, -): ApplicationStateMachine { +): ApplicationStateMachine => { const context = initialContext ? { ...initialContext, application } : { application } diff --git a/libs/application/types/src/lib/application-types.ts b/libs/application/types/src/lib/application-types.ts index 243e1d2afa38..aecee156a270 100644 --- a/libs/application/types/src/lib/application-types.ts +++ b/libs/application/types/src/lib/application-types.ts @@ -1,3 +1,3 @@ -export function applicationTypes(): string { +export const applicationTypes = (): string => { return 'application-types' } diff --git a/libs/application/ui-components/src/utilities/FileUploadController/index.tsx b/libs/application/ui-components/src/utilities/FileUploadController/index.tsx index ddd5a75b0932..25d69e923d16 100644 --- a/libs/application/ui-components/src/utilities/FileUploadController/index.tsx +++ b/libs/application/ui-components/src/utilities/FileUploadController/index.tsx @@ -36,7 +36,7 @@ const answerToUploadFile = ({ name, key }: UploadFile): UploadFile => { return { name, key, status: 'done' } } -function reducer(state: UploadFile[], action: Action) { +const reducer = (state: UploadFile[], action: Action) => { switch (action.type) { case ActionTypes.ADD: return state.concat(action.payload.newFiles) diff --git a/libs/application/ui-components/src/utilities/handleServerError.ts b/libs/application/ui-components/src/utilities/handleServerError.ts index 79ccd95f4b83..3ade7b66c9f4 100644 --- a/libs/application/ui-components/src/utilities/handleServerError.ts +++ b/libs/application/ui-components/src/utilities/handleServerError.ts @@ -8,10 +8,10 @@ import { toast } from '@island.is/island-ui/core' import { FormatMessage } from '@island.is/localization' import { findProblemInApolloError } from '@island.is/shared/problem' -export function handleServerError( +export const handleServerError = ( error: ApolloError, formatMessage: FormatMessage, -): void { +): void => { const problem = findProblemInApolloError(error) const message = problem ? problem.detail ?? problem.title : error.message diff --git a/libs/application/ui-shell/src/components/DelegationsScreen.tsx b/libs/application/ui-shell/src/components/DelegationsScreen.tsx index 8eda93405a2d..37b76da98f6e 100644 --- a/libs/application/ui-shell/src/components/DelegationsScreen.tsx +++ b/libs/application/ui-shell/src/components/DelegationsScreen.tsx @@ -68,7 +68,7 @@ export const DelegationsScreen = ({ // Check whether application supports delegations useEffect(() => { - async function applicationSupportsDelegations() { + const applicationSupportsDelegations = async () => { if (type) { const template = await getApplicationTemplateByTypeId(type) const featureFlagEnabled = await featureFlagClient.getValue( diff --git a/libs/application/ui-shell/src/components/FormRepeater.tsx b/libs/application/ui-shell/src/components/FormRepeater.tsx index 0c3acb330a7a..93c71a84ff4a 100644 --- a/libs/application/ui-shell/src/components/FormRepeater.tsx +++ b/libs/application/ui-shell/src/components/FormRepeater.tsx @@ -50,7 +50,7 @@ const FormRepeater: FC< [], ) as RepeaterItems - async function removeRepeaterItem(index: number) { + const removeRepeaterItem = async (index: number) => { if (index >= 0 && index < repeaterItems.length) { const newRepeaterItems = [ ...repeaterItems.slice(0, index), @@ -60,7 +60,7 @@ const FormRepeater: FC< } } - async function setRepeaterItems(items: RepeaterItems) { + const setRepeaterItems = async (items: RepeaterItems) => { return await onUpdateRepeater(items) } diff --git a/libs/application/ui-shell/src/components/ScreenFooter.tsx b/libs/application/ui-shell/src/components/ScreenFooter.tsx index 276f7a2c781a..4e4ed12dd51a 100644 --- a/libs/application/ui-shell/src/components/ScreenFooter.tsx +++ b/libs/application/ui-shell/src/components/ScreenFooter.tsx @@ -85,7 +85,7 @@ export const ScreenFooter: FC> = ({ return null } - function renderSubmitButtons() { + const renderSubmitButtons = () => { if (!submitField || submitField.placement === 'screen') { return (