diff --git a/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.spec.ts b/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.spec.ts index 54cd32b08d75..bc0b9c64d676 100644 --- a/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.spec.ts +++ b/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.spec.ts @@ -14,7 +14,7 @@ import { Defendant } from '../../defendant' import { Case } from '../models/case.model' import { getAppealInfo, - getDefendantsInfo, + getIndictmentDefendantsInfo, getIndictmentInfo, transformCase, } from './case.transformer' @@ -685,23 +685,25 @@ describe('getIndictmentInfo', () => { }) }) -describe('getDefentandInfo', () => { - it('should add verdict appeal deadline for defendants with verdict view date', () => { +describe('getIndictmentDefendantsInfo', () => { + it('should add verdict appeal deadline and expiry for defendants with verdict view date', () => { const defendants = [ { verdictViewDate: '2022-06-15T19:50:08.033Z' } as Defendant, { verdictViewDate: undefined } as Defendant, ] - const defendantsInfo = getDefendantsInfo(defendants) + const defendantsInfo = getIndictmentDefendantsInfo(defendants) expect(defendantsInfo).toEqual([ { verdictViewDate: '2022-06-15T19:50:08.033Z', verdictAppealDeadline: '2022-07-13T19:50:08.033Z', + isVerdictAppealDeadlineExpired: true, }, { verdictViewDate: undefined, verdictAppealDeadline: undefined, + isVerdictAppealDeadlineExpired: false, }, ]) }) diff --git a/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.ts b/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.ts index 4accbfef14f9..f3f952dbdd2c 100644 --- a/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.ts +++ b/apps/judicial-system/api/src/app/modules/case/interceptors/case.transformer.ts @@ -169,7 +169,9 @@ export const getIndictmentInfo = ( return indictmentInfo } -export const getDefendantsInfo = (defendants: Defendant[] | undefined) => { +export const getIndictmentDefendantsInfo = ( + defendants: Defendant[] | undefined, +) => { return defendants?.map((defendant) => { const { verdictViewDate } = defendant const verdictAppealDeadline = verdictViewDate @@ -177,10 +179,14 @@ export const getDefendantsInfo = (defendants: Defendant[] | undefined) => { new Date(verdictViewDate).getTime() + getDays(28), ).toISOString() : undefined + const isVerdictAppealDeadlineExpired = verdictAppealDeadline + ? Date.now() >= new Date(verdictAppealDeadline).getTime() + : false return { ...defendant, verdictAppealDeadline, + isVerdictAppealDeadlineExpired, } }) } @@ -194,7 +200,7 @@ const transformIndictmentCase = (theCase: Case): Case => { theCase.defendants, theCase.eventLogs, ), - defendants: getDefendantsInfo(theCase.defendants), + defendants: getIndictmentDefendantsInfo(theCase.defendants), } } diff --git a/apps/judicial-system/api/src/app/modules/case/interceptors/limitedAccessCase.transformer.ts b/apps/judicial-system/api/src/app/modules/case/interceptors/limitedAccessCase.transformer.ts index c7b4828ba946..befd415e8905 100644 --- a/apps/judicial-system/api/src/app/modules/case/interceptors/limitedAccessCase.transformer.ts +++ b/apps/judicial-system/api/src/app/modules/case/interceptors/limitedAccessCase.transformer.ts @@ -1,10 +1,15 @@ import { CaseState, completedRequestCaseStates, + isRequestCase, RequestSharedWithDefender, } from '@island.is/judicial-system/types' import { Case } from '../models/case.model' +import { + getIndictmentDefendantsInfo, + getIndictmentInfo, +} from './case.transformer' const RequestSharedWithDefenderAllowedStates: { [key in RequestSharedWithDefender]: CaseState[] @@ -39,7 +44,7 @@ export const canDefenderViewRequest = (theCase: Case) => { ) } -export const transformLimitedAccessCase = (theCase: Case): Case => { +const transformRequestCase = (theCase: Case): Case => { return { ...theCase, caseResentExplanation: canDefenderViewRequest(theCase) @@ -47,3 +52,26 @@ export const transformLimitedAccessCase = (theCase: Case): Case => { : undefined, } } + +const transformIndictmentCase = (theCase: Case): Case => { + const { indictmentRulingDecision, rulingDate, defendants, eventLogs } = + theCase + return { + ...theCase, + ...getIndictmentInfo( + indictmentRulingDecision, + rulingDate, + defendants, + eventLogs, + ), + defendants: getIndictmentDefendantsInfo(theCase.defendants), + } +} + +export const transformLimitedAccessCase = (theCase: Case): Case => { + if (isRequestCase(theCase.type)) { + return transformRequestCase(theCase) + } + + return transformIndictmentCase(theCase) +} diff --git a/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts b/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts index 5b509b4dba2b..b592ccccb6e9 100644 --- a/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts +++ b/apps/judicial-system/api/src/app/modules/defendant/models/defendant.model.ts @@ -72,6 +72,9 @@ export class Defendant { @Field(() => String, { nullable: true }) readonly verdictAppealDeadline?: string + @Field(() => Boolean, { nullable: true }) + readonly isVerdictAppealDeadlineExpired?: boolean + @Field(() => DefenderChoice, { nullable: true }) readonly defenderChoice?: DefenderChoice diff --git a/apps/judicial-system/web/src/components/FormProvider/case.graphql b/apps/judicial-system/web/src/components/FormProvider/case.graphql index 5f2d3f3ec668..752280bf625c 100644 --- a/apps/judicial-system/web/src/components/FormProvider/case.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/case.graphql @@ -30,6 +30,7 @@ query Case($input: CaseQueryInput!) { serviceRequirement verdictViewDate verdictAppealDeadline + isVerdictAppealDeadlineExpired subpoenaType subpoenas { id diff --git a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql index 4eda4e1c3493..06bce52f67c8 100644 --- a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql +++ b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql @@ -34,8 +34,10 @@ query LimitedAccessCase($input: CaseQueryInput!) { defenderEmail defenderPhoneNumber defenderChoice + serviceRequirement verdictViewDate verdictAppealDeadline + isVerdictAppealDeadlineExpired subpoenas { id created @@ -166,6 +168,8 @@ query LimitedAccessCase($input: CaseQueryInput!) { indictmentRulingDecision indictmentCompletedDate indictmentReviewDecision + indictmentVerdictViewedByAll + indictmentVerdictAppealDeadlineExpired indictmentReviewer { id name diff --git a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.spec.ts b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.spec.ts index 5333f47cbe61..816e28ff4388 100644 --- a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.spec.ts +++ b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.spec.ts @@ -15,10 +15,12 @@ describe('DefendantInfo', () => { test('should return the correct string if serviceRequirement is REQUIRED and verdictAppealDeadline is not provided', () => { const verdictAppealDeadline = undefined + const isVerdictAppealDeadlineExpired = false const serviceRequirement = ServiceRequirement.REQUIRED const dataSections = getAppealExpirationInfo( verdictAppealDeadline, + isVerdictAppealDeadlineExpired, serviceRequirement, ) @@ -29,10 +31,13 @@ describe('DefendantInfo', () => { test('should return the correct string if serviceRequirement is NOT_APPLICABLE and verdictAppealDeadline is not provided', () => { const verdictAppealDeadline = undefined + const isVerdictAppealDeadlineExpired = false + const serviceRequirement = ServiceRequirement.NOT_APPLICABLE const dataSections = getAppealExpirationInfo( verdictAppealDeadline, + isVerdictAppealDeadlineExpired, serviceRequirement, ) @@ -43,10 +48,12 @@ describe('DefendantInfo', () => { test('should return the correct string if serviceRequirement is NOT_REQUIRED', () => { const verdictAppealDeadline = undefined + const isVerdictAppealDeadlineExpired = false const serviceRequirement = ServiceRequirement.NOT_REQUIRED const dataSections = getAppealExpirationInfo( verdictAppealDeadline, + isVerdictAppealDeadlineExpired, serviceRequirement, ) @@ -57,10 +64,12 @@ describe('DefendantInfo', () => { test('should return the correct string if serviceRequirement is REQUIRED and appeal expiration date is in the future', () => { const verdictAppealDeadline = '2024-08-05' + const isVerdictAppealDeadlineExpired = false const serviceRequirement = ServiceRequirement.REQUIRED const dataSections = getAppealExpirationInfo( verdictAppealDeadline, + isVerdictAppealDeadlineExpired, serviceRequirement, ) @@ -72,10 +81,12 @@ describe('DefendantInfo', () => { test('should return the correct string if serviceRequirement is NOT_APPLICABLE and appeal expiration date is in the future', () => { const verdictAppealDeadline = '2024-08-05' + const isVerdictAppealDeadlineExpired = false const serviceRequirement = ServiceRequirement.NOT_APPLICABLE const dataSections = getAppealExpirationInfo( verdictAppealDeadline, + isVerdictAppealDeadlineExpired, serviceRequirement, ) @@ -87,10 +98,12 @@ describe('DefendantInfo', () => { test('should return the correct string if serviceRequirement is REQUIRED and appeal expiration date is in the past', () => { const verdictAppealDeadline = '2024-07-07' + const isVerdictAppealDeadlineExpired = true const serviceRequirement = ServiceRequirement.REQUIRED const dataSections = getAppealExpirationInfo( verdictAppealDeadline, + isVerdictAppealDeadlineExpired, serviceRequirement, ) @@ -102,10 +115,12 @@ describe('DefendantInfo', () => { test('should return the correct string if serviceRequirement is NOT_APPLICABLE and appeal expiration date is in the past', () => { const verdictAppealDeadline = '2024-07-07' + const isVerdictAppealDeadlineExpired = true const serviceRequirement = ServiceRequirement.NOT_APPLICABLE const dataSections = getAppealExpirationInfo( verdictAppealDeadline, + isVerdictAppealDeadlineExpired, serviceRequirement, ) diff --git a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx index 1666858b4342..dbbade62a3cb 100644 --- a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx +++ b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.tsx @@ -38,6 +38,7 @@ interface DefendantInfoProps { export const getAppealExpirationInfo = ( verdictAppealDeadline?: string | null, + isVerdictAppealDeadlineExpired?: boolean | null, serviceRequirement?: ServiceRequirement | null, ) => { if (serviceRequirement === ServiceRequirement.NOT_REQUIRED) { @@ -48,14 +49,11 @@ export const getAppealExpirationInfo = ( return { message: strings.appealDateNotBegun, date: null } } - // TODO: Move to the server as today may not be accurate in the client - const today = new Date() const expiryDate = new Date(verdictAppealDeadline) - const message = - today < expiryDate - ? strings.appealExpirationDate - : strings.appealDateExpired + const message = isVerdictAppealDeadlineExpired + ? strings.appealDateExpired + : strings.appealExpirationDate return { message, date: formatDate(expiryDate) } } @@ -72,6 +70,7 @@ export const DefendantInfo: FC = (props) => { const appealExpirationInfo = getAppealExpirationInfo( defendant.verdictAppealDeadline, + defendant.isVerdictAppealDeadlineExpired, defendant.serviceRequirement, ) diff --git a/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegation-index-test-cases.ts b/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegation-index-test-cases.ts index 75b143ce5fd9..c474205a2f64 100644 --- a/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegation-index-test-cases.ts +++ b/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegation-index-test-cases.ts @@ -1,13 +1,34 @@ +import { generatePerson } from 'kennitala' +import faker from 'faker' + import { createClient } from '@island.is/services/auth/testing' import { AuthDelegationType } from '@island.is/shared/types' import { createNationalId } from '@island.is/testing/fixtures' import { clientId, TestCase } from './delegations-index-types' +const YEAR = 1000 * 60 * 60 * 24 * 365 +export const testDate = new Date(2024, 2, 1) +const today = new Date() + const adult1 = createNationalId('residentAdult') const adult2 = createNationalId('residentAdult') -const child1 = createNationalId('residentChild') -const child2 = createNationalId('residentChild') +const child1 = generatePerson( + new Date( + Date.now() - faker.datatype.number({ min: 17 * YEAR, max: 18 * YEAR }), + ), +) // between 17-18 years old +export const child2 = generatePerson( + new Date( + Date.now() - faker.datatype.number({ min: 1 * YEAR, max: 15 * YEAR }), + ), +) // under 16 years old +const child3 = generatePerson( + new Date(today.getFullYear() - 16, today.getMonth(), today.getDate()), +) // exactly 16 years old +const child4 = generatePerson( + new Date(today.getFullYear() - 18, today.getMonth(), today.getDate()), +) // exactly 18 years old const company1 = createNationalId('company') const company2 = createNationalId('company') export const prRight1 = 'pr1' @@ -22,7 +43,10 @@ export const indexingTestCases: Record = { }), { fromCustom: [adult1, adult2], - expectedFrom: [adult1, adult2], + expectedFrom: [ + { nationalId: adult1, type: AuthDelegationType.Custom }, + { nationalId: adult2, type: AuthDelegationType.Custom }, + ], }, ), // Should index legal guardian delegations @@ -34,7 +58,11 @@ export const indexingTestCases: Record = { }), { fromChildren: [child2, child1], - expectedFrom: [child2, child1], + expectedFrom: [ + { nationalId: child2, type: AuthDelegationType.LegalGuardian }, + { nationalId: child2, type: AuthDelegationType.LegalGuardianMinor }, + { nationalId: child1, type: AuthDelegationType.LegalGuardian }, + ], // gets both LegalGuardian and LegalGuardianMinor delegation types for child2 (since they are under 16 years old) }, ), // should not index if child is 18 eighteen years old ward2: new TestCase( @@ -48,6 +76,28 @@ export const indexingTestCases: Record = { expectedFrom: [], }, ), + ward3: new TestCase( + createClient({ + clientId: clientId, + supportsLegalGuardians: true, + }), + { + fromChildren: [child3], // exactly 16 years old + expectedFrom: [ + { nationalId: child3, type: AuthDelegationType.LegalGuardian }, + ], + }, + ), + ward4: new TestCase( + createClient({ + clientId: clientId, + supportsLegalGuardians: true, + }), + { + fromChildren: [child4], // exactly 18 years old + expectedFrom: [], + }, + ), // Should index procuration holders delegations company: new TestCase( createClient({ @@ -57,7 +107,10 @@ export const indexingTestCases: Record = { }), { fromCompanies: [company1, company2], - expectedFrom: [company1, company2], + expectedFrom: [ + { nationalId: company1, type: AuthDelegationType.ProcurationHolder }, + { nationalId: company2, type: AuthDelegationType.ProcurationHolder }, + ], }, ), // Should index personal representatives delegations @@ -71,7 +124,12 @@ export const indexingTestCases: Record = { fromRepresentative: [ { fromNationalId: adult1, rightTypes: [{ code: prRight1 }] }, ], - expectedFrom: [adult1], + expectedFrom: [ + { + nationalId: adult1, + type: `${AuthDelegationType.PersonalRepresentative}:${prRight1}` as AuthDelegationType, + }, + ], }, ), singleCustomDelegation: new TestCase( @@ -82,7 +140,7 @@ export const indexingTestCases: Record = { }), { fromCustom: [adult1], - expectedFrom: [adult1], + expectedFrom: [{ nationalId: adult1, type: AuthDelegationType.Custom }], }, ), customTwoDomains: new TestCase( @@ -94,7 +152,7 @@ export const indexingTestCases: Record = { { fromCustom: [adult1], fromCustomOtherDomain: [adult1], - expectedFrom: [adult1], + expectedFrom: [{ nationalId: adult1, type: AuthDelegationType.Custom }], }, ), } diff --git a/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegation-index.controller.spec.ts b/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegation-index.controller.spec.ts index 10afbe6133d2..aa041b8b0c1c 100644 --- a/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegation-index.controller.spec.ts +++ b/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegation-index.controller.spec.ts @@ -14,6 +14,8 @@ import { } from '@island.is/shared/types' import { setupWithAuth } from '../../../../../test/setup' +import kennitala from 'kennitala' +import addYears from 'date-fns/addYears' const path = '/v1/delegation-index/.id' const testNationalId = createNationalId('person') @@ -413,4 +415,90 @@ describe('DelegationIndexController', () => { expect(delegation).toBeNull() }) }) + + describe('PUT for Legal guardians', () => { + let app: TestApp + let server: request.SuperTest + + let delegationIndexModel: typeof DelegationIndex + const delegationProvider = AuthDelegationProvider.NationalRegistry + const user = createCurrentUser({ + nationalIdType: 'person', + scope: [AuthScope.delegationIndexWrite], + delegationProvider: delegationProvider as AuthDelegationProvider, + }) + + beforeAll(async () => { + app = await setupWithAuth({ + user, + }) + server = request(app.getHttpServer()) + + delegationIndexModel = app.get(getModelToken(DelegationIndex)) + }) + + afterAll(async () => { + await app.cleanUp() + }) + + it('PUT - should create LegalGuardianMinor delegation record if creating LegalGuardian delegation for child under 16', async () => { + // Arrange + const DATE_OF_BIRTH = new Date(addYears(new Date(), -15).toDateString()) + jest.spyOn(kennitala, 'isValid').mockReturnValue(true) + jest.spyOn(kennitala, 'info').mockReturnValue({ + kt: '0101302399', + valid: true, + type: 'person', + birthday: DATE_OF_BIRTH, + birthdayReadable: 'any', + age: 15, + }) + + const { toNationalId, fromNationalId, type } = { + toNationalId: user.nationalId, + fromNationalId: '0101302399', + type: AuthDelegationType.LegalGuardian, + } + + // Act + const response = await server + .put(path) + .set('X-Param-Id', `${type}_${toNationalId}_${fromNationalId}`) + .send() + + // Assert + const legalGuardianDelegation = await delegationIndexModel.findOne({ + where: { + fromNationalId: fromNationalId, + toNationalId: toNationalId, + type: type, + provider: delegationProvider, + }, + }) + const legalGuardianMinorDelegation = await delegationIndexModel.findOne({ + where: { + fromNationalId: fromNationalId, + toNationalId: toNationalId, + type: AuthDelegationType.LegalGuardianMinor, + provider: delegationProvider, + }, + }) + + expect(response.status).toBe(200) + expect(response.body.fromNationalId).toBe(fromNationalId) + expect(response.body.toNationalId).toBe(toNationalId) + expect(legalGuardianDelegation).toBeDefined() + expect(legalGuardianMinorDelegation).toBeDefined() + expect(legalGuardianDelegation?.validTo).toStrictEqual( + // 18 years from kid's birthday + addYears(DATE_OF_BIRTH, 18), + ) + expect(legalGuardianMinorDelegation?.validTo).toStrictEqual( + // 16 years from kid's birthday + addYears(DATE_OF_BIRTH, 16), + ) + + jest.resetAllMocks() + }) + }) }) diff --git a/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegation-index.service.spec.ts b/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegation-index.service.spec.ts index d265b3d64fb3..f1b3bd93d526 100644 --- a/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegation-index.service.spec.ts +++ b/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegation-index.service.spec.ts @@ -3,6 +3,20 @@ import { getConnectionToken, getModelToken } from '@nestjs/sequelize' import faker from 'faker' import { Sequelize } from 'sequelize-typescript' +import { + indexingTestCases, + prRight1, + testDate, +} from './delegation-index-test-cases' +import { setupWithAuth } from '../../../../../test/setup' +import { + customScopes, + customScopesOtherDomain, + domainName, + TestCase, + user, +} from './delegations-index-types' + import { actorSubjectIdType, audkenniProvider, @@ -24,18 +38,6 @@ import { import { createNationalRegistryUser } from '@island.is/testing/fixtures' import { TestApp, truncate } from '@island.is/testing/nest' -import { setupWithAuth } from '../../../../../test/setup' -import { indexingTestCases, prRight1 } from './delegation-index-test-cases' -import { - customScopes, - customScopesOtherDomain, - domainName, - TestCase, - user, -} from './delegations-index-types' - -const testDate = new Date(2024, 2, 1) - describe('DelegationsIndexService', () => { let app: TestApp let delegationIndexService: DelegationsIndexService @@ -222,8 +224,12 @@ describe('DelegationsIndexService', () => { expect(delegations.length).toBe(testCase.expectedFrom.length) delegations.forEach((delegation) => { - expect(testCase.expectedFrom).toContain(delegation.fromNationalId) - expect(delegation.toNationalId).toBe(user.nationalId) + const delegationRecord = testCase.expectedFrom.find( + (record) => + record.nationalId === delegation.fromNationalId && + record.type === delegation.type, + ) + expect(delegationRecord).toBeDefined() }) }) }, @@ -240,11 +246,10 @@ describe('DelegationsIndexService', () => { // Act await delegationIndexService.indexDelegations(user) - try { - await delegationIndexService.indexDelegations(user) - } catch (error) { - console.log(error) - } + // delete delegation index meta to force reindex + await delegationIndexMetaModel.destroy({ where: {} }) + + await delegationIndexService.indexDelegations(user) // Assert const delegations = await delegationIndexModel.findAll({ @@ -442,7 +447,7 @@ describe('DelegationsIndexService', () => { describe('indexCustomDelegations', () => { const testCase = indexingTestCases.custom - beforeAll(async () => { + beforeEach(async () => { await setup(testCase) }) @@ -462,8 +467,41 @@ describe('DelegationsIndexService', () => { expect(delegations.length).toEqual(testCase.expectedFrom.length) delegations.forEach((delegation) => { - expect(testCase.expectedFrom).toContain(delegation.fromNationalId) - expect(delegation.toNationalId).toBe(user.nationalId) + const delegationRecord = testCase.expectedFrom.find( + (record) => + record.nationalId === delegation.fromNationalId && + record.type === delegation.type, + ) + expect(delegationRecord).toBeDefined() + }) + }) + + it('should not fail when re-indexing', async () => { + // Arrange + const nationalId = user.nationalId + + // Act + await delegationIndexService.indexCustomDelegations(nationalId, user) + + await delegationIndexMetaModel.destroy({ where: {} }) + + await delegationIndexService.indexCustomDelegations(nationalId, user) + + // Assert + const delegations = await delegationIndexModel.findAll({ + where: { + toNationalId: nationalId, + }, + }) + + expect(delegations.length).toEqual(testCase.expectedFrom.length) + delegations.forEach((delegation) => { + const delegationRecord = testCase.expectedFrom.find( + (record) => + record.nationalId === delegation.fromNationalId && + record.type === delegation.type, + ) + expect(delegationRecord).toBeDefined() }) }) }) @@ -471,8 +509,12 @@ describe('DelegationsIndexService', () => { describe('indexRepresentativeDelegations', () => { const testCase = indexingTestCases.personalRepresentative - beforeAll(async () => { - await setup(testCase) + beforeEach(async () => await setup(testCase)) + + afterEach(async () => { + // remove all data + await delegationIndexMetaModel.destroy({ where: {} }) + await delegationIndexModel.destroy({ where: {} }) }) it('should index personal representation delegations', async () => { @@ -494,11 +536,13 @@ describe('DelegationsIndexService', () => { expect(delegations.length).toEqual(testCase.expectedFrom.length) delegations.forEach((delegation) => { - expect(testCase.expectedFrom).toContain(delegation.fromNationalId) - expect(delegation.toNationalId).toBe(user.nationalId) - expect(delegation.type).toBe( - `${AuthDelegationType.PersonalRepresentative}:${prRight1}`, + const delegationRecord = testCase.expectedFrom.find( + (record) => + record.nationalId === delegation.fromNationalId && + (record.type as any) === + `${AuthDelegationType.PersonalRepresentative}:${prRight1}`, ) + expect(delegationRecord).toBeDefined() }) }) }) @@ -647,7 +691,9 @@ describe('DelegationsIndexService', () => { const indexedDelegation = indexedDelegations[0] - expect(indexedDelegation.fromNationalId).toBe(testCase.expectedFrom[0]) + expect(indexedDelegation.fromNationalId).toBe( + testCase.expectedFrom[0].nationalId, + ) expect(indexedDelegation.toNationalId).toBe(user.nationalId) expect(indexedDelegation.customDelegationScopes).toHaveLength(4) expect(indexedDelegation.customDelegationScopes?.sort()).toEqual( diff --git a/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegations-index-types.ts b/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegations-index-types.ts index 6588d66f7c1f..79d06ec88107 100644 --- a/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegations-index-types.ts +++ b/apps/services/auth/delegation-api/src/app/delegations/test/delegation-index/delegations-index-types.ts @@ -32,7 +32,7 @@ export interface ITestCaseOptions { fromRepresentative?: CreatePersonalRepresentativeDelegation[] scopes?: string[] scopeAccess?: [string, string][] - expectedFrom: string[] + expectedFrom: { nationalId: string; type: AuthDelegationType }[] representativeRights?: string[] } @@ -49,7 +49,7 @@ export class TestCase { scopes: string[] scopesOtherDomain: string[] scopeAccess: [string, string][] - expectedFrom: string[] + expectedFrom: { nationalId: string; type: AuthDelegationType }[] constructor(client: CreateClient, options: ITestCaseOptions) { this.client = client diff --git a/apps/services/auth/delegation-api/src/app/delegations/test/delegations-controller/delegations.controller.test-cases.ts b/apps/services/auth/delegation-api/src/app/delegations/test/delegations-controller/delegations.controller.test-cases.ts index c08e03d314a9..7177412e19ac 100644 --- a/apps/services/auth/delegation-api/src/app/delegations/test/delegations-controller/delegations.controller.test-cases.ts +++ b/apps/services/auth/delegation-api/src/app/delegations/test/delegations-controller/delegations.controller.test-cases.ts @@ -14,7 +14,7 @@ const person2 = createNationalId('person') const person3 = createNationalId('person') const Parent1 = createNationalId('person') -const child1 = createNationalId('person') +const child1 = createNationalId('residentChild') const company1 = createNationalId('company') diff --git a/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters-test-cases.ts b/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters-test-cases.ts index f18686a29cc6..8922a9558f32 100644 --- a/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters-test-cases.ts +++ b/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters-test-cases.ts @@ -11,8 +11,8 @@ import { TestCase, } from './delegations-filters-types' -const person1 = createNationalId('person') -const person2 = createNationalId('person') +const person1 = createNationalId('residentAdult') +const person2 = createNationalId('residentAdult') const company1 = createNationalId('company') const company2 = createNationalId('company') diff --git a/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts b/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts index b178739555d5..d57f16dea37d 100644 --- a/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts +++ b/apps/services/auth/public-api/src/app/modules/delegations/actorDelegations.controller.spec.ts @@ -1,6 +1,8 @@ import { getModelToken } from '@nestjs/sequelize' import times from 'lodash/times' import request from 'supertest' +import kennitala from 'kennitala' +import addYears from 'date-fns/addYears' import { ClientDelegationType, @@ -20,7 +22,10 @@ import { } from '@island.is/auth-api-lib' import { AuthScope } from '@island.is/auth/scopes' import { RskRelationshipsClient } from '@island.is/clients-rsk-relationships' -import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2' +import { + IndividualDto, + NationalRegistryClientService, +} from '@island.is/clients/national-registry-v2' import { createClient, createDelegation, @@ -180,8 +185,8 @@ describe('ActorDelegationsController', () => { nationalRegistryApi = app.get(NationalRegistryClientService) }) - beforeEach(() => { - return clientDelegationTypeModel.bulkCreate( + beforeEach(async () => { + return await clientDelegationTypeModel.bulkCreate( delegationTypes.map((type) => ({ clientId: client.clientId, delegationType: type, @@ -676,10 +681,32 @@ describe('ActorDelegationsController', () => { describe('with legal guardian delegations', () => { let getForsja: jest.SpyInstance - beforeAll(() => { - const client = app.get(NationalRegistryClientService) + let clientInstance: any + + const mockForKt = (kt: string): void => { + jest.spyOn(kennitala, 'info').mockReturnValue({ + kt, + age: 16, + birthday: addYears(Date.now(), -15), + birthdayReadable: '', + type: 'person', + valid: true, + }) + + jest.spyOn(clientInstance, 'getIndividual').mockResolvedValueOnce({ + nationalId: kt, + name: nationalRegistryUser.name, + } as IndividualDto) + + jest + .spyOn(clientInstance, 'getCustodyChildren') + .mockResolvedValueOnce([kt]) + } + + beforeEach(() => { + clientInstance = app.get(NationalRegistryClientService) getForsja = jest - .spyOn(client, 'getCustodyChildren') + .spyOn(clientInstance, 'getCustodyChildren') .mockResolvedValue([nationalRegistryUser.nationalId]) }) @@ -688,23 +715,29 @@ describe('ActorDelegationsController', () => { }) it('should return delegations', async () => { + const kt = '1111089030' + // Arrange - const expectedDelegation = { + mockForKt(kt) + + const expectedDelegation = DelegationDTOMapper.toMergedDelegationDTO({ fromName: nationalRegistryUser.name, - fromNationalId: nationalRegistryUser.nationalId, - provider: 'thjodskra', + fromNationalId: kt, + provider: AuthDelegationProvider.NationalRegistry, toNationalId: user.nationalId, - type: 'LegalGuardian', - } as DelegationDTO + type: [ + AuthDelegationType.LegalGuardian, + AuthDelegationType.LegalGuardianMinor, + ], + } as Omit & { type: AuthDelegationType | AuthDelegationType[] }) + // Act const res = await server.get(`${path}${query}`) // Assert expect(res.status).toEqual(200) expect(res.body).toHaveLength(1) - expect(res.body[0]).toEqual( - DelegationDTOMapper.toMergedDelegationDTO(expectedDelegation), - ) + expect(res.body[0]).toEqual(expectedDelegation) }) it('should not return delegations when client does not support legal guardian delegations', async () => { diff --git a/apps/services/auth/public-api/test/setup.ts b/apps/services/auth/public-api/test/setup.ts index 5cd632add54c..eb533a696e10 100644 --- a/apps/services/auth/public-api/test/setup.ts +++ b/apps/services/auth/public-api/test/setup.ts @@ -70,6 +70,7 @@ interface SetupOptions { export const delegationTypes = [ AuthDelegationType.Custom, AuthDelegationType.LegalGuardian, + AuthDelegationType.LegalGuardianMinor, AuthDelegationType.ProcurationHolder, AuthDelegationType.PersonalRepresentative, AuthDelegationType.GeneralMandate, diff --git a/apps/skilavottord/ws/src/app/modules/auth/auth.guard.ts b/apps/skilavottord/ws/src/app/modules/auth/auth.guard.ts index e63b768a315b..1c667edf14e4 100644 --- a/apps/skilavottord/ws/src/app/modules/auth/auth.guard.ts +++ b/apps/skilavottord/ws/src/app/modules/auth/auth.guard.ts @@ -22,6 +22,8 @@ import { RolesGuard } from './roles.guard' import { Role } from './user.model' import { logger } from '@island.is/logging' +import { isRunningOnEnvironment } from '@island.is/shared/utils' + type AuthorizeOptions = { roles?: Role[] } @@ -73,12 +75,18 @@ export class AuthGuard implements CanActivate { export const Authorize = ( { roles = [] }: AuthorizeOptions = { roles: [] }, ): MethodDecorator & ClassDecorator => { + logger.info(`car-recycling: AuthGuard environment #1`, { + environment: process.env.NODE_ENV, + isProduction: isRunningOnEnvironment('production'), + }) + // IdsUserGuard is causing constant reload on local and DEV in the skilavottord-web // To 'fix' it for now we just skip using it for non production - if (process.env.NODE_ENV !== 'production') { - logger.info(`AuthGuard environment`, { - environment: process.env.NODE_ENV, - }) + if ( + process.env.NODE_ENV !== 'production' || + !isRunningOnEnvironment('production') + ) { + logger.info('`car-recycling: AuthGuard - skipping IdsUserGuard') return applyDecorators( SetMetadata('roles', roles), UseGuards(AuthGuard, RolesGuard), diff --git a/apps/web/screens/Article/Article.tsx b/apps/web/screens/Article/Article.tsx index b78ff4fc6cca..c1bf6981d61c 100644 --- a/apps/web/screens/Article/Article.tsx +++ b/apps/web/screens/Article/Article.tsx @@ -1,68 +1,70 @@ import { FC, useEffect, useMemo, useRef, useState } from 'react' -import { useRouter } from 'next/router' +import { createPortal } from 'react-dom' import NextLink from 'next/link' +import { useRouter } from 'next/router' import { BLOCKS } from '@contentful/rich-text-types' import slugify from '@sindresorhus/slugify' + import { - Slice as SliceType, ProcessEntry, + Slice as SliceType, } from '@island.is/island-ui/contentful' import { Box, - Text, - Stack, + type BreadCrumbItem, Breadcrumbs, + Button, GridColumn, GridRow, Link, Navigation, + Stack, TableOfContents, - Button, Tag, + Text, } from '@island.is/island-ui/core' +import { Locale } from '@island.is/shared/types' import { + AppendedArticleComponents, HeadWithSocialSharing, InstitutionPanel, InstitutionsPanel, OrganizationFooter, - Sticky, - Webreader, - AppendedArticleComponents, + SignLanguageButton, Stepper, stepperUtils, - Form, - SignLanguageButton, + Sticky, + Webreader, } from '@island.is/web/components' -import { withMainLayout } from '@island.is/web/layouts/main' -import { GET_ARTICLE_QUERY, GET_NAMESPACE_QUERY } from '../queries' -import { Screen } from '@island.is/web/types' -import { useNamespace, usePlausiblePageview } from '@island.is/web/hooks' -import { useI18n } from '@island.is/web/i18n' -import { CustomNextError } from '@island.is/web/units/errors' -import { - QueryGetNamespaceArgs, - GetNamespaceQuery, +import type { AllSlicesFragment as Slice, + GetNamespaceQuery, GetSingleArticleQuery, - QueryGetSingleArticleArgs, Organization, + QueryGetNamespaceArgs, + QueryGetSingleArticleArgs, Stepper as StepperSchema, } from '@island.is/web/graphql/schema' -import { createNavigation } from '@island.is/web/utils/navigation' +import { useNamespace, usePlausiblePageview } from '@island.is/web/hooks' import useContentfulId from '@island.is/web/hooks/useContentfulId' -import { SidebarLayout } from '../Layouts/SidebarLayout' -import { createPortal } from 'react-dom' +import { useI18n } from '@island.is/web/i18n' +import { withMainLayout } from '@island.is/web/layouts/main' +import type { Screen } from '@island.is/web/types' +import { CustomNextError } from '@island.is/web/units/errors' +import { createNavigation } from '@island.is/web/utils/navigation' +import { getOrganizationLink } from '@island.is/web/utils/organization' +import { webRichText } from '@island.is/web/utils/richText' + import { LinkResolverResponse, LinkType, useLinkResolver, } from '../../hooks/useLinkResolver' -import { ArticleChatPanel } from './components/ArticleChatPanel' -import { webRichText } from '@island.is/web/utils/richText' -import { Locale } from '@island.is/shared/types' import { useScrollPosition } from '../../hooks/useScrollPosition' import { scrollTo } from '../../hooks/useScrollSpy' -import { getOrganizationLink } from '@island.is/web/utils/organization' +import { SidebarLayout } from '../Layouts/SidebarLayout' +import { GET_ARTICLE_QUERY, GET_NAMESPACE_QUERY } from '../queries' +import { ArticleChatPanel } from './components/ArticleChatPanel' type Article = GetSingleArticleQuery['getSingleArticle'] // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -318,7 +320,7 @@ const ArticleSidebar: FC> = ({ export interface ArticleProps { article: Article - namespace: GetNamespaceQuery['getNamespace'] + namespace: Record // eslint-disable-next-line @typescript-eslint/no-explicit-any stepOptionsFromNamespace: { data: Record[]; slug: string }[] stepperNamespace: GetNamespaceQuery['getNamespace'] @@ -344,8 +346,6 @@ const ArticleScreen: Screen = ({ processEntryRef.current = document.querySelector('#processRef') setMounted(true) }, []) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error make web strict const n = useNamespace(namespace) const { query, asPath } = useRouter() const { linkResolver } = useLinkResolver() @@ -419,38 +419,62 @@ const ArticleScreen: Screen = ({ const organizationShortTitle = article?.organization?.[0]?.shortTitle const inStepperView = useMemo( - () => query.stepper === 'true' && !!article?.stepper, - [query.stepper, article?.stepper], + () => + query.stepper === 'true' && (!!article?.stepper || !!subArticle?.stepper), + [query.stepper, article?.stepper, subArticle?.stepper], ) - const breadcrumbItems = useMemo( - () => - inStepperView - ? [] - : [ - { - title: 'Ísland.is', - typename: 'homepage', - href: '/', - }, - !!article?.category?.slug && { - title: article.category.title, - typename: 'articlecategory', - slug: [article.category.slug], - }, - !!article?.category?.slug && - !!article.group && { - isTag: true, - title: article.group.title, - typename: 'articlecategory', - slug: [ - article.category.slug + - (article.group?.slug ? `#${article.group.slug}` : ''), - ], - }, + const breadcrumbItems = useMemo(() => { + const items: BreadCrumbItem[] = [] + + if (inStepperView) { + if (!article) { + return items + } + + items.push({ + title: article.title, + typename: 'article', + slug: [article.slug], + }) + if (subArticle) { + items.push({ + title: subArticle.title, + typename: 'subarticle', + slug: subArticle.slug.split('/'), + }) + } + + return items + } + + items.push({ + title: 'Ísland.is', + typename: 'homepage', + href: '/', + }) + + if (article?.category?.slug) { + items.push({ + title: article.category.title, + typename: 'articlecategory', + slug: [article.category.slug], + }) + if (!!article?.category?.slug && !!article.group) { + items.push({ + isTag: true, + title: article.group.title, + typename: 'articlecategory', + slug: [ + article.category.slug + + (article.group?.slug ? `#${article.group.slug}` : ''), ], - [article?.category, article?.group, inStepperView], - ) + }) + } + } + + return items + }, [article, inStepperView, subArticle]) const content = ( @@ -470,7 +494,11 @@ const ArticleScreen: Screen = ({ '', )} processLink={asPath.split('?')[0].concat('?stepper=true')} - processTitle={article?.stepper?.title ?? ''} + processTitle={ + subArticle + ? subArticle.stepper?.title ?? '' + : article?.stepper?.title ?? '' + } newTab={false} /> @@ -592,21 +620,11 @@ const ArticleScreen: Screen = ({ > - {inStepperView && ( - - - {article?.title} - - - )} - - {!inStepperView && ( + { return ( @@ -620,6 +638,16 @@ const ArticleScreen: Screen = ({ ) }} /> + + {inStepperView && ( + + + )} = ({ @@ -733,14 +765,16 @@ const ArticleScreen: Screen = ({ )} - - - + {!inStepperView && ( + + + + )} {processEntry?.processLink && ( = ({ )} - {subArticle && ( + {!inStepperView && subArticle && ( {subArticle.title} diff --git a/apps/web/screens/queries/Article.ts b/apps/web/screens/queries/Article.ts index 159dca98d6c3..b11036282513 100644 --- a/apps/web/screens/queries/Article.ts +++ b/apps/web/screens/queries/Article.ts @@ -133,6 +133,21 @@ export const GET_ARTICLE_QUERY = gql` ${nestedFields} } showTableOfContents + stepper { + id + title + steps { + id + title + slug + stepType + subtitle { + ...AllSlices + } + config + } + config + } } featuredImage { url diff --git a/libs/api/domains/auth/src/lib/models/delegation.model.ts b/libs/api/domains/auth/src/lib/models/delegation.model.ts index 48065a23658b..db0f3d7ffa2c 100644 --- a/libs/api/domains/auth/src/lib/models/delegation.model.ts +++ b/libs/api/domains/auth/src/lib/models/delegation.model.ts @@ -26,6 +26,8 @@ const exhaustiveCheck = (param: never) => { switch (delegation.type) { case AuthDelegationType.LegalGuardian: return LegalGuardianDelegation + case AuthDelegationType.LegalGuardianMinor: + return LegalGuardianMinorDelegation case AuthDelegationType.ProcurationHolder: return ProcuringHolderDelegation case AuthDelegationType.PersonalRepresentative: @@ -69,6 +71,11 @@ export abstract class Delegation { }) export class LegalGuardianDelegation extends Delegation {} +@ObjectType('AuthLegalGuardianMinorDelegation', { + implements: Delegation, +}) +export class LegalGuardianMinorDelegation extends Delegation {} + @ObjectType('AuthProcuringHolderDelegation', { implements: Delegation, }) diff --git a/libs/application/templates/driving-license-duplicate/src/lib/drivingLicenseDuplicateTemplate.ts b/libs/application/templates/driving-license-duplicate/src/lib/drivingLicenseDuplicateTemplate.ts index 81e4b7bde2c1..cff350026e7b 100644 --- a/libs/application/templates/driving-license-duplicate/src/lib/drivingLicenseDuplicateTemplate.ts +++ b/libs/application/templates/driving-license-duplicate/src/lib/drivingLicenseDuplicateTemplate.ts @@ -119,7 +119,11 @@ const DrivingLicenseDuplicateTemplate: ApplicationTemplate< api: [ CurrentLicenseApi, JurisdictionApi, - NationalRegistryUserApi, + NationalRegistryUserApi.configure({ + params: { + legalDomicileIceland: true, + }, + }), SyslumadurPaymentCatalogApi, QualitySignatureApi, QualityPhotoApi, diff --git a/libs/application/templates/driving-license/src/forms/draft/subSectionOtherCountry.ts b/libs/application/templates/driving-license/src/forms/draft/subSectionOtherCountry.ts index 00ba5a0bd979..109ab38b7120 100644 --- a/libs/application/templates/driving-license/src/forms/draft/subSectionOtherCountry.ts +++ b/libs/application/templates/driving-license/src/forms/draft/subSectionOtherCountry.ts @@ -50,9 +50,7 @@ export const subSectionOtherCountry = buildSubSection({ { value: NO, label: m.noDeprivedDrivingLicenseInOtherCountryTitle, - subLabel: - m.noDeprivedDrivingLicenseInOtherCountryDescription - .defaultMessage, + subLabel: m.noDeprivedDrivingLicenseInOtherCountryDescription, }, ], }), diff --git a/libs/application/templates/driving-license/src/forms/prerequisites/sectionApplicationFor.ts b/libs/application/templates/driving-license/src/forms/prerequisites/sectionApplicationFor.ts index a5ba5d014713..cf6ba528b6bb 100644 --- a/libs/application/templates/driving-license/src/forms/prerequisites/sectionApplicationFor.ts +++ b/libs/application/templates/driving-license/src/forms/prerequisites/sectionApplicationFor.ts @@ -74,15 +74,13 @@ export const sectionApplicationFor = ( let options = [ { label: m.applicationForTempLicenseTitle, - subLabel: - m.applicationForTempLicenseDescription.defaultMessage, + subLabel: m.applicationForTempLicenseDescription, value: B_TEMP, disabled: !!currentLicense, }, { label: m.applicationForFullLicenseTitle, - subLabel: - m.applicationForFullLicenseDescription.defaultMessage, + subLabel: m.applicationForFullLicenseDescription, value: B_FULL, disabled: !currentLicense, }, @@ -91,8 +89,7 @@ export const sectionApplicationFor = ( if (allow65Renewal) { options = options.concat({ label: m.applicationForRenewalLicenseTitle, - subLabel: - m.applicationForRenewalLicenseDescription.defaultMessage, + subLabel: m.applicationForRenewalLicenseDescription, value: B_FULL_RENEWAL_65, disabled: !currentLicense || age < 65, }) @@ -101,7 +98,7 @@ export const sectionApplicationFor = ( if (allowBELicense) { options = options.concat({ label: m.applicationForBELicenseTitle, - subLabel: m.applicationForBELicenseDescription.defaultMessage, + subLabel: m.applicationForBELicenseDescription, value: BE, disabled: !currentLicense || diff --git a/libs/application/templates/driving-license/src/lib/messages.ts b/libs/application/templates/driving-license/src/lib/messages.ts index 5bb5c988c112..07431e977201 100644 --- a/libs/application/templates/driving-license/src/lib/messages.ts +++ b/libs/application/templates/driving-license/src/lib/messages.ts @@ -641,15 +641,15 @@ export const m = defineMessages({ defaultMessage: 'Veldu sýslumannsembætti', description: 'Choose district commissioner', }, - chooseDistrictCommisionerForFullLicense: { - id: 'dl.application:chooseDistrictCommisionerForFullLicense', + chooseDistrictCommissionerForFullLicense: { + id: 'dl.application:chooseDistrictCommissionerForFullLicense', defaultMessage: - 'Veldu það embætti sýslumanns þar sem þú vilt skila inn bráðabirgðaskírteini og fá afhent nýtt fullnaðarskírteini', + 'Veldu það embætti sýslumanns þar sem þú vilt skila inn eldra ökuskírteini og fá afhent nýtt með nýjum réttindunum', description: 'Choose district commissioner for returning a temporary license and recieve a new full license', }, - chooseDistrictCommisionerForTempLicense: { - id: 'dl.application:chooseDistrictCommisionerForTempLicense', + chooseDistrictCommissionerForTempLicense: { + id: 'dl.application:chooseDistrictCommissionerForTempLicense', defaultMessage: 'Veldu það embætti sýslumanns sem þú hyggst skila inn gæðamerktri ljósmynd', description: 'Choose district commissioner for submitting a quality photo', diff --git a/libs/application/templates/driving-license/src/lib/utils/formUtils.ts b/libs/application/templates/driving-license/src/lib/utils/formUtils.ts index 69897b5d9bae..00a0b876c073 100644 --- a/libs/application/templates/driving-license/src/lib/utils/formUtils.ts +++ b/libs/application/templates/driving-license/src/lib/utils/formUtils.ts @@ -62,8 +62,8 @@ export const chooseDistrictCommissionerDescription = ({ ) === B_TEMP return applicationForTemp - ? m.chooseDistrictCommisionerForTempLicense.defaultMessage - : m.chooseDistrictCommisionerForFullLicense.defaultMessage + ? m.chooseDistrictCommissionerForTempLicense + : m.chooseDistrictCommissionerForFullLicense } export const hasCompletedPrerequisitesStep = diff --git a/libs/application/templates/estate/src/forms/Done.ts b/libs/application/templates/estate/src/forms/Done.ts index 960effe812aa..9d84f1617f32 100644 --- a/libs/application/templates/estate/src/forms/Done.ts +++ b/libs/application/templates/estate/src/forms/Done.ts @@ -46,8 +46,8 @@ export const done: Form = buildForm({ id: 'goToServicePortal', title: '', url: '/minarsidur/umsoknir', - buttonTitle: coreMessages.openServicePortalButtonTitle, - message: coreMessages.openServicePortalMessageText, + buttonTitle: m.openServicePortalTitle, + message: m.openServicePortalMessage, }), ], }), diff --git a/libs/application/templates/estate/src/lib/messages.ts b/libs/application/templates/estate/src/lib/messages.ts index 77f7f2629314..f946b20423fb 100644 --- a/libs/application/templates/estate/src/lib/messages.ts +++ b/libs/application/templates/estate/src/lib/messages.ts @@ -963,7 +963,7 @@ export const m = defineMessages({ description: '', }, notFilledOutItalic: { - id: 'es.application:notFilledOut#markdown', + id: 'es.application:notFilledOutItalic#markdown', defaultMessage: '*Ekki fyllt út*', description: '', }, @@ -1005,6 +1005,17 @@ export const m = defineMessages({ 'Sýslumaður hefur móttekið beiðni þína um einkaskipti. Hún verður nú tekin til afgreiðslu og upplýsingar um afgreiðslu beiðninnar send í pósthólf þitt á Ísland.is.', description: '', }, + openServicePortalTitle: { + id: 'es.application:openServicePortalTitle', + defaultMessage: 'Mínar síður', + description: '', + }, + openServicePortalMessage: { + id: 'es.application:openServicePortalMessage', + defaultMessage: + 'Inni á Mínum síðum og í Ísland.is appinu hefur þú aðgang að þínum upplýsingum, Stafrænu pósthólfi og stöðu umsóknar.', + description: '', + }, // Validation errors errorPhoneNumber: { diff --git a/libs/application/templates/inheritance-report/src/lib/messages.ts b/libs/application/templates/inheritance-report/src/lib/messages.ts index 9c0d900d2423..b4716cfdfad5 100644 --- a/libs/application/templates/inheritance-report/src/lib/messages.ts +++ b/libs/application/templates/inheritance-report/src/lib/messages.ts @@ -425,7 +425,7 @@ export const m = defineMessages({ description: '', }, realEstateDescriptionPrePaid: { - id: 'ir.application:realEstateDescriptionPrePaid', + id: 'ir.application:realEstateDescriptionPrePaid#markdown', defaultMessage: 'Til dæmis íbúðarhús, sumarhús, lóðir og jarðir.', description: '', }, diff --git a/libs/auth-api-lib/migrations/20240607093733-add-legal-guardian-minor-delegation-type.js b/libs/auth-api-lib/migrations/20240607093733-add-legal-guardian-minor-delegation-type.js new file mode 100644 index 000000000000..996368a6a42b --- /dev/null +++ b/libs/auth-api-lib/migrations/20240607093733-add-legal-guardian-minor-delegation-type.js @@ -0,0 +1,16 @@ +'use strict' + +module.exports = { + async up(queryInterface, Sequelize) { + return queryInterface.sequelize.query(` + INSERT INTO "delegation_type" (id, name, description, provider) + VALUES ('LegalGuardianMinor', 'Legal Guardian minor', 'A legal guardian of a minor under 16 years', 'thjodskra'); + `) + }, + + async down(queryInterface, Sequelize) { + return queryInterface.sequelize.query(` + DELETE FROM "delegation_type" WHERE id = 'LegalGuardianMinor'; + `) + }, +} diff --git a/libs/auth-api-lib/src/index.ts b/libs/auth-api-lib/src/index.ts index 28b1b0007819..c31273fdae15 100644 --- a/libs/auth-api-lib/src/index.ts +++ b/libs/auth-api-lib/src/index.ts @@ -187,6 +187,7 @@ export * from './lib/personal-representative/dto/paginated-personal-representati export * from './lib/personal-representative/dto/personal-representative-scope-permission.dto' export * from './lib/clients/admin/dto/admin-create-client.dto' export * from './lib/clients/admin/dto/admin-client.dto' +export { isUnderXAge } from './lib/delegations/utils/isUnderXAge' // Passkeys core module export * from './lib/passkeys-core/passkeys-core.module' diff --git a/libs/auth-api-lib/src/lib/delegations/delegation-dto.mapper.ts b/libs/auth-api-lib/src/lib/delegations/delegation-dto.mapper.ts index 807fb8efde13..77e3d051b961 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegation-dto.mapper.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegation-dto.mapper.ts @@ -1,16 +1,21 @@ import { DelegationRecordDTO } from './dto/delegation-index.dto' import { DelegationDTO } from './dto/delegation.dto' import { MergedDelegationDTO } from './dto/merged-delegation.dto' +import { AuthDelegationType } from '@island.is/shared/types' export class DelegationDTOMapper { - public static toMergedDelegationDTO(dto: DelegationDTO): MergedDelegationDTO { + public static toMergedDelegationDTO( + dto: Omit & { + type: AuthDelegationType | AuthDelegationType[] + }, + ): MergedDelegationDTO { return { fromName: dto.fromName, fromNationalId: dto.fromNationalId, toNationalId: dto.toNationalId, toName: dto.toName, validTo: dto.validTo, - types: [dto.type], + types: [dto.type].flat(), scopes: dto.scopes, } } diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-incoming-ward.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-incoming-ward.service.ts index d71902f82093..02e83397ecf8 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-incoming-ward.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-incoming-ward.service.ts @@ -1,5 +1,9 @@ import { Inject, Injectable, Logger } from '@nestjs/common' +import { isUnderXAge } from './utils/isUnderXAge' +import { ApiScopeInfo } from './delegations-incoming.service' +import { DelegationDTO } from './dto/delegation.dto' + import { User } from '@island.is/auth-nest-tools' import { IndividualDto, @@ -11,9 +15,6 @@ import { AuthDelegationType, } from '@island.is/shared/types' -import { ApiScopeInfo } from './delegations-incoming.service' -import { DelegationDTO } from './dto/delegation.dto' - @Injectable() export class DelegationsIncomingWardService { constructor( @@ -55,7 +56,8 @@ export class DelegationsIncomingWardService { const result = await Promise.all(resultPromises) - return result + // delegations for legal guardians of children under 18 + const legalGuardianDelegations = result .filter((p): p is IndividualDto => p !== null) .map( (p) => @@ -67,6 +69,16 @@ export class DelegationsIncomingWardService { provider: AuthDelegationProvider.NationalRegistry, }, ) + + // delegations for legal guardians of children under 16 + const legalGuardianMinorDelegations = legalGuardianDelegations + .filter((delegation) => isUnderXAge(16, delegation.fromNationalId)) + .map((delegation) => ({ + ...delegation, + type: AuthDelegationType.LegalGuardianMinor, + })) + + return [...legalGuardianDelegations, ...legalGuardianMinorDelegations] } catch (error) { this.logger.error('Error in findAllWards', error) } diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-index.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-index.service.ts index 22747d838416..64efc98e373e 100644 --- a/libs/auth-api-lib/src/lib/delegations/delegations-index.service.ts +++ b/libs/auth-api-lib/src/lib/delegations/delegations-index.service.ts @@ -1,9 +1,9 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common' import { InjectModel } from '@nestjs/sequelize' import startOfDay from 'date-fns/startOfDay' -import * as kennitala from 'kennitala' import union from 'lodash/union' import { Op } from 'sequelize' +import * as kennitala from 'kennitala' import { Auth, User } from '@island.is/auth-nest-tools' import { AuditService } from '@island.is/nest/audit' @@ -40,6 +40,8 @@ import { validateDelegationTypeAndProvider, validateToAndFromNationalId, } from './utils/delegations' +import { getXBirthday } from './utils/getXBirthday' +import { isUnderXAge } from './utils/isUnderXAge' const TEN_MINUTES = 1000 * 60 * 10 const ONE_WEEK = 1000 * 60 * 60 * 24 * 7 @@ -82,27 +84,6 @@ type FetchDelegationRecordsArgs = { direction: DelegationDirection } -const getTimeUntilEighteen = (nationalId: string) => { - const birthDate = kennitala.info(nationalId).birthday - - if (!birthDate) { - return null - } - - const now = startOfDay(new Date()) - const eighteen = startOfDay( - new Date( - birthDate.getFullYear() + 18, - birthDate.getMonth(), - birthDate.getDate(), - ), - ) - - const timeUntilEighteen = eighteen.getTime() - now.getTime() - - return timeUntilEighteen > 0 ? new Date(timeUntilEighteen) : null -} - const validateCrudParams = (delegation: DelegationRecordInputDTO) => { if (!validateDelegationTypeAndProvider(delegation)) { throw new BadRequestException( @@ -307,6 +288,24 @@ export class DelegationsIndexService { ) { validateCrudParams(delegation) + // legal guardian delegations have a validTo date + if (delegation.type === AuthDelegationType.LegalGuardian) { + // ensure delegation only exists if child is under 18 + if (!delegation.validTo) { + delegation.validTo = getXBirthday(18, delegation.fromNationalId) + } + + // create additional delegation for children under 16 + const isMinor = isUnderXAge(16, delegation.fromNationalId) + if (isMinor) { + await this.delegationIndexModel.upsert({ + ...delegation, + type: AuthDelegationType.LegalGuardianMinor, + validTo: getXBirthday(16, delegation.fromNationalId), + }) + } + } + const [updatedDelegation] = await this.auditService.auditPromise( { auth, @@ -411,7 +410,7 @@ export class DelegationsIndexService { currRecords, }) - await Promise.all([ + const indexingPromises = await Promise.allSettled([ this.delegationIndexModel.bulkCreate(created), updated.map((d) => this.delegationIndexModel.update(d, { @@ -435,6 +434,13 @@ export class DelegationsIndexService { ), ]) + // log any errors + indexingPromises.forEach((p) => { + if (p.status === 'rejected') { + console.error(p.reason) + } + }) + // saveToIndex is used by multiple entry points, when indexing so this // is the common place to audit updates in the index. this.auditService.audit({ @@ -462,6 +468,7 @@ export class DelegationsIndexService { (acc, curr) => { const existing = currRecords.find( (d) => + d.toNationalId === curr.toNationalId && d.fromNationalId === curr.fromNationalId && d.type === curr.type && d.provider === curr.provider, @@ -622,10 +629,13 @@ export class DelegationsIndexService { .map((delegation) => toDelegationIndexInfo({ ...delegation, - validTo: getTimeUntilEighteen(delegation.fromNationalId), // validTo is the date the child turns 18 + validTo: + delegation.type === AuthDelegationType.LegalGuardian + ? getXBirthday(18, delegation.fromNationalId) // validTo is the date the child turns 18 for legal guardian delegations + : getXBirthday(16, delegation.fromNationalId), // validTo is the date the child turns 16 for legal guardian minor delegations }), ) - .filter((d) => d.validTo !== null), // if child has already turned 18, we don't want to index the delegation + .filter((d) => d.validTo !== null), // if child has already turned 18/16, we don't want to index the delegation ) } diff --git a/libs/auth-api-lib/src/lib/delegations/utils/delegations.ts b/libs/auth-api-lib/src/lib/delegations/utils/delegations.ts index f4f36b46df77..63b326218341 100644 --- a/libs/auth-api-lib/src/lib/delegations/utils/delegations.ts +++ b/libs/auth-api-lib/src/lib/delegations/utils/delegations.ts @@ -16,7 +16,10 @@ export const delegationProviderTypeMap: Record< AuthDelegationProvider, DelegationRecordType[] > = { - [AuthDelegationProvider.NationalRegistry]: [AuthDelegationType.LegalGuardian], + [AuthDelegationProvider.NationalRegistry]: [ + AuthDelegationType.LegalGuardian, + AuthDelegationType.LegalGuardianMinor, + ], [AuthDelegationProvider.CompanyRegistry]: [ AuthDelegationType.ProcurationHolder, ], diff --git a/libs/auth-api-lib/src/lib/delegations/utils/getXBirthday.ts b/libs/auth-api-lib/src/lib/delegations/utils/getXBirthday.ts new file mode 100644 index 000000000000..11c85be9f967 --- /dev/null +++ b/libs/auth-api-lib/src/lib/delegations/utils/getXBirthday.ts @@ -0,0 +1,24 @@ +import kennitala from 'kennitala' +import startOfDay from 'date-fns/startOfDay' +import isBefore from 'date-fns/isBefore' + +/* Gets the date when a person turns X age */ +export const getXBirthday = (age: number, nationalId: string) => { + const birthDate = kennitala.info(nationalId).birthday + + // The date when the person turns X age + const xBirthday = startOfDay( + new Date( + birthDate.getFullYear() + age, + birthDate.getMonth(), + birthDate.getDate(), + ), + ) + + // If a person hasn't already turned X age + if (isBefore(new Date(), xBirthday)) { + return xBirthday + } + + return null +} diff --git a/libs/auth-api-lib/src/lib/delegations/utils/isUnderXAge.ts b/libs/auth-api-lib/src/lib/delegations/utils/isUnderXAge.ts new file mode 100644 index 000000000000..90cf4641bac0 --- /dev/null +++ b/libs/auth-api-lib/src/lib/delegations/utils/isUnderXAge.ts @@ -0,0 +1,18 @@ +import kennitala from 'kennitala' +import startOfDay from 'date-fns/startOfDay' + +export const isUnderXAge = (age: number, nationalId: string) => { + const birthDate = kennitala.info(nationalId).birthday + const now = startOfDay(new Date()) + const eighteen = startOfDay( + new Date( + birthDate.getFullYear() + age, + birthDate.getMonth(), + birthDate.getDate(), + ), + ) + + const timeUntilAge = eighteen.getTime() - now.getTime() + + return timeUntilAge > 0 +} diff --git a/libs/cms/src/lib/generated/contentfulTypes.d.ts b/libs/cms/src/lib/generated/contentfulTypes.d.ts index 9301bded9a0d..2efeba12a455 100644 --- a/libs/cms/src/lib/generated/contentfulTypes.d.ts +++ b/libs/cms/src/lib/generated/contentfulTypes.d.ts @@ -3951,7 +3951,12 @@ export interface IStepFields { slug: string /** Step Type */ - stepType?: 'Question - Radio' | 'Question - Dropdown' | 'Answer' | undefined + stepType?: + | 'Question - Radio' + | 'Question - Dropdown' + | 'Information' + | 'Answer' + | undefined /** Subtitle */ subtitle?: Document | undefined @@ -4107,6 +4112,9 @@ export interface ISubArticleFields { /** Sign Language Video */ signLanguageVideo?: IEmbeddedVideo | undefined + + /** Stepper */ + stepper?: IStepper | undefined } /** A sub article that's a part of another main article */ diff --git a/libs/cms/src/lib/models/subArticle.model.ts b/libs/cms/src/lib/models/subArticle.model.ts index 4a58296e67fc..6be71388e4fe 100644 --- a/libs/cms/src/lib/models/subArticle.model.ts +++ b/libs/cms/src/lib/models/subArticle.model.ts @@ -5,6 +5,7 @@ import { ISubArticle } from '../generated/contentfulTypes' import { mapDocument, SliceUnion } from '../unions/slice.union' import { ArticleReference, mapArticleReference } from './articleReference' import { EmbeddedVideo, mapEmbeddedVideo } from './embeddedVideo.model' +import { mapStepper, Stepper } from './stepper.model' @ObjectType() export class SubArticle { @@ -28,6 +29,9 @@ export class SubArticle { @CacheField(() => EmbeddedVideo, { nullable: true }) signLanguageVideo?: EmbeddedVideo | null + + @CacheField(() => Stepper, { nullable: true }) + stepper?: Stepper | null } export const mapSubArticle = ({ @@ -52,5 +56,6 @@ export const mapSubArticle = ({ signLanguageVideo: fields.signLanguageVideo ? mapEmbeddedVideo(fields.signLanguageVideo) : null, + stepper: fields.stepper ? mapStepper(fields.stepper) : null, } } diff --git a/libs/cms/src/lib/search/importers/manualChapterItem.service.ts b/libs/cms/src/lib/search/importers/manualChapterItem.service.ts index 5ba4b02ce5d6..0819af47bf2f 100644 --- a/libs/cms/src/lib/search/importers/manualChapterItem.service.ts +++ b/libs/cms/src/lib/search/importers/manualChapterItem.service.ts @@ -22,7 +22,7 @@ export class ManualChapterItemSyncService implements CmsSyncProvider { return entries.filter((entry) => { return ( isManual(entry) && - entry.fields.chapters.length > 0 && + entry.fields.chapters?.length > 0 && entry.fields.chapters.some( (chapter) => chapter.fields.title && diff --git a/libs/island-ui/core/src/lib/Breadcrumbs/Breadcrumbs.tsx b/libs/island-ui/core/src/lib/Breadcrumbs/Breadcrumbs.tsx index a965a128107e..70e1e9f45f33 100644 --- a/libs/island-ui/core/src/lib/Breadcrumbs/Breadcrumbs.tsx +++ b/libs/island-ui/core/src/lib/Breadcrumbs/Breadcrumbs.tsx @@ -59,7 +59,11 @@ export const Breadcrumbs: FC> = ({ ) return ( - + {isLink ? renderLink(