diff --git a/apps/application-system/api/src/app/modules/application/e2e/payment/payment-callback.spec.ts b/apps/application-system/api/src/app/modules/application/e2e/payment/payment-callback.spec.ts
index 4f3a44d88b0b..7352887abc88 100644
--- a/apps/application-system/api/src/app/modules/application/e2e/payment/payment-callback.spec.ts
+++ b/apps/application-system/api/src/app/modules/application/e2e/payment/payment-callback.spec.ts
@@ -16,18 +16,16 @@ beforeAll(async () => {
describe('Application system payments callback API', () => {
// Sets the payment status to paid.
it(`POST /application-payment/32eee126-6b7f-4fca-b9a0-a3618b3e42bf/6b11dc9f-a694-440e-b3dd-7163b5f34815 should update payment fulfilled`, async () => {
- await server
+ const response = await server
.post(
'/application-payment/32eee126-6b7f-4fca-b9a0-a3618b3e42bf/6b11dc9f-a694-440e-b3dd-7163b5f34815',
)
.send({
- callback: {
- receptionID: '1234567890',
- chargeItemSubject: 'Very nice subject',
- status: 'paid',
- },
+ receptionID: '123e4567-e89b-12d3-a456-426614174000', // Updated to real UUID
+ chargeItemSubject: 'Very nice subject',
+ status: 'paid',
})
- .expect(201)
+ expect(response.status).toBe(201)
})
// Fails to set the payment status to paid.
@@ -37,11 +35,9 @@ describe('Application system payments callback API', () => {
'/application-payment/32eee126-6b7f-4fca-b9a0-a3618b3e42bf/missing-id',
)
.send({
- callback: {
- receptionID: '1234567890',
- chargeItemSubject: 'nice subject.. not',
- status: 'paid',
- },
+ receptionID: '123e4567-e89b-12d3-a456-426614174000', // Updated to real UUID
+ chargeItemSubject: 'nice subject.. not',
+ status: 'paid',
})
.expect(400)
})
diff --git a/apps/application-system/api/src/openApi.ts b/apps/application-system/api/src/openApi.ts
index 1c22ee62d9ab..fa773e20e102 100644
--- a/apps/application-system/api/src/openApi.ts
+++ b/apps/application-system/api/src/openApi.ts
@@ -8,5 +8,6 @@ export const openApi = new DocumentBuilder()
.setVersion('1.0')
.addTag('application')
.addTag('payment')
+ .addTag('payment-callback')
.addBearerAuth()
.build()
diff --git a/apps/judicial-system/api/src/app/modules/backend/backend.service.ts b/apps/judicial-system/api/src/app/modules/backend/backend.service.ts
index 238f4b51de76..5a42b29ab8a7 100644
--- a/apps/judicial-system/api/src/app/modules/backend/backend.service.ts
+++ b/apps/judicial-system/api/src/app/modules/backend/backend.service.ts
@@ -269,8 +269,16 @@ export class BackendService extends DataSource<{ req: Request }> {
return this.post(`case/${id}/file`, createFile)
}
- getCaseFileSignedUrl(caseId: string, id: string): Promise {
- return this.get(`case/${caseId}/file/${id}/url`)
+ getCaseFileSignedUrl(
+ caseId: string,
+ id: string,
+ mergedCaseId?: string,
+ ): Promise {
+ const mergedCaseInjection = mergedCaseId
+ ? `/mergedCase/${mergedCaseId}`
+ : ''
+
+ return this.get(`case/${caseId}${mergedCaseInjection}/file/${id}/url`)
}
deleteCaseFile(caseId: string, id: string): Promise {
@@ -426,8 +434,15 @@ export class BackendService extends DataSource<{ req: Request }> {
limitedAccessGetCaseFileSignedUrl(
caseId: string,
id: string,
+ mergedCaseId?: string,
): Promise {
- return this.get(`case/${caseId}/limitedAccess/file/${id}/url`)
+ const mergedCaseInjection = mergedCaseId
+ ? `/mergedCase/${mergedCaseId}`
+ : ''
+
+ return this.get(
+ `case/${caseId}/limitedAccess${mergedCaseInjection}/file/${id}/url`,
+ )
}
limitedAccessDeleteCaseFile(
diff --git a/apps/judicial-system/api/src/app/modules/file/dto/getSignedUrl.input.ts b/apps/judicial-system/api/src/app/modules/file/dto/getSignedUrl.input.ts
index 0718c486c7d8..23bce59da4a7 100644
--- a/apps/judicial-system/api/src/app/modules/file/dto/getSignedUrl.input.ts
+++ b/apps/judicial-system/api/src/app/modules/file/dto/getSignedUrl.input.ts
@@ -1,4 +1,4 @@
-import { Allow } from 'class-validator'
+import { Allow, IsOptional } from 'class-validator'
import { Field, ID, InputType } from '@nestjs/graphql'
@@ -11,4 +11,9 @@ export class GetSignedUrlInput {
@Allow()
@Field(() => ID)
readonly caseId!: string
+
+ @Allow()
+ @IsOptional()
+ @Field(() => ID, { nullable: true })
+ readonly mergedCaseId?: string
}
diff --git a/apps/judicial-system/api/src/app/modules/file/file.controller.ts b/apps/judicial-system/api/src/app/modules/file/file.controller.ts
index ab341cdd1364..34e61e6ad93c 100644
--- a/apps/judicial-system/api/src/app/modules/file/file.controller.ts
+++ b/apps/judicial-system/api/src/app/modules/file/file.controller.ts
@@ -54,10 +54,14 @@ export class FileController {
)
}
- @Get('caseFilesRecord/:policeCaseNumber')
+ @Get([
+ 'caseFilesRecord/:policeCaseNumber',
+ 'mergedCase/:mergedCaseId/caseFilesRecord/:policeCaseNumber',
+ ])
@Header('Content-Type', 'application/pdf')
getCaseFilesRecordPdf(
@Param('id') id: string,
+ @Param('mergedCaseId') mergedCaseId: string,
@Param('policeCaseNumber') policeCaseNumber: string,
@CurrentHttpUser() user: User,
@Req() req: Request,
@@ -65,11 +69,15 @@ export class FileController {
): Promise {
this.logger.debug(`Getting the case files for case ${id} as a pdf document`)
+ const mergedCaseInjection = mergedCaseId
+ ? `mergedCase/${mergedCaseId}/`
+ : ''
+
return this.fileService.tryGetFile(
user.id,
AuditedAction.GET_CASE_FILES_PDF,
id,
- `caseFilesRecord/${policeCaseNumber}`,
+ `${mergedCaseInjection}caseFilesRecord/${policeCaseNumber}`,
req,
res,
'pdf',
@@ -143,21 +151,26 @@ export class FileController {
)
}
- @Get('indictment')
+ @Get(['indictment', 'mergedCase/:mergedCaseId/indictment'])
@Header('Content-Type', 'application/pdf')
getIndictmentPdf(
@Param('id') id: string,
+ @Param('mergedCaseId') mergedCaseId: string,
@CurrentHttpUser() user: User,
@Req() req: Request,
@Res() res: Response,
): Promise {
this.logger.debug(`Getting the indictment for case ${id} as a pdf document`)
+ const mergedCaseInjection = mergedCaseId
+ ? `mergedCase/${mergedCaseId}/`
+ : ''
+
return this.fileService.tryGetFile(
user.id,
AuditedAction.GET_INDICTMENT_PDF,
id,
- 'indictment',
+ `${mergedCaseInjection}indictment`,
req,
res,
'pdf',
diff --git a/apps/judicial-system/api/src/app/modules/file/file.resolver.ts b/apps/judicial-system/api/src/app/modules/file/file.resolver.ts
index 574b00b67e1d..45990fbb7dc2 100644
--- a/apps/judicial-system/api/src/app/modules/file/file.resolver.ts
+++ b/apps/judicial-system/api/src/app/modules/file/file.resolver.ts
@@ -85,14 +85,14 @@ export class FileResolver {
@Context('dataSources')
{ backendService }: { backendService: BackendService },
): Promise {
- const { caseId, id } = input
+ const { caseId, id, mergedCaseId } = input
this.logger.debug(`Getting a signed url for file ${id} of case ${caseId}`)
return this.auditTrailService.audit(
user.id,
AuditedAction.GET_SIGNED_URL,
- backendService.getCaseFileSignedUrl(caseId, id),
+ backendService.getCaseFileSignedUrl(caseId, id, mergedCaseId),
id,
)
}
diff --git a/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.controller.ts b/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.controller.ts
index e3ee2e902257..247f47218ef4 100644
--- a/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.controller.ts
+++ b/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.controller.ts
@@ -6,6 +6,7 @@ import {
Header,
Inject,
Param,
+ Query,
Req,
Res,
UseGuards,
@@ -19,7 +20,7 @@ import {
CurrentHttpUser,
JwtInjectBearerAuthGuard,
} from '@island.is/judicial-system/auth'
-import type { User } from '@island.is/judicial-system/types'
+import type { SubpoenaType, User } from '@island.is/judicial-system/types'
import { FileService } from './file.service'
@@ -52,10 +53,14 @@ export class LimitedAccessFileController {
)
}
- @Get('caseFilesRecord/:policeCaseNumber')
+ @Get([
+ 'caseFilesRecord/:policeCaseNumber',
+ 'mergedCase/:mergedCaseId/caseFilesRecord/:policeCaseNumber',
+ ])
@Header('Content-Type', 'application/pdf')
async getCaseFilesRecordPdf(
@Param('id') id: string,
+ @Param('mergedCaseId') mergedCaseId: string,
@Param('policeCaseNumber') policeCaseNumber: string,
@CurrentHttpUser() user: User,
@Req() req: Request,
@@ -63,11 +68,15 @@ export class LimitedAccessFileController {
): Promise {
this.logger.debug(`Getting the case files for case ${id} as a pdf document`)
+ const mergedCaseInjection = mergedCaseId
+ ? `mergedCase/${mergedCaseId}/`
+ : ''
+
return this.fileService.tryGetFile(
user.id,
AuditedAction.GET_CASE_FILES_PDF,
id,
- `limitedAccess/caseFilesRecord/${policeCaseNumber}`,
+ `limitedAccess/${mergedCaseInjection}caseFilesRecord/${policeCaseNumber}`,
req,
res,
'pdf',
@@ -141,21 +150,53 @@ export class LimitedAccessFileController {
)
}
- @Get('indictment')
+ @Get(['indictment', 'mergedCase/:mergedCaseId/indictment'])
@Header('Content-Type', 'application/pdf')
async getIndictmentPdf(
@Param('id') id: string,
+ @Param('mergedCaseId') mergedCaseId: string,
@CurrentHttpUser() user: User,
@Req() req: Request,
@Res() res: Response,
): Promise {
this.logger.debug(`Getting the indictment for case ${id} as a pdf document`)
+ const mergedCaseInjection = mergedCaseId
+ ? `mergedCase/${mergedCaseId}/`
+ : ''
+
return this.fileService.tryGetFile(
user.id,
AuditedAction.GET_INDICTMENT_PDF,
id,
- 'limitedAccess/indictment',
+ `limitedAccess/${mergedCaseInjection}indictment`,
+ req,
+ res,
+ 'pdf',
+ )
+ }
+
+ @Get('subpoena/:defendantId')
+ @Header('Content-Type', 'application/pdf')
+ getSubpoenaPdf(
+ @Param('id') id: string,
+ @Param('defendantId') defendantId: string,
+ @Query('arraignmentDate') arraignmentDate: string,
+ @Query('location') location: string,
+ @Query('subpoenaType') subpoenaType: SubpoenaType,
+ @CurrentHttpUser() user: User,
+ @Req() req: Request,
+ @Res() res: Response,
+ ): Promise {
+ this.logger.debug(
+ `Getting the subpoena for defendant ${defendantId} of case ${id} as a pdf document`,
+ )
+
+ return this.fileService.tryGetFile(
+ user.id,
+ AuditedAction.GET_SUBPOENA_PDF,
+ id,
+ `limitedAccess/defendant/${defendantId}/subpoena?arraignmentDate=${arraignmentDate}&location=${location}&subpoenaType=${subpoenaType}`,
req,
res,
'pdf',
diff --git a/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.resolver.ts b/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.resolver.ts
index 2d6fb1e19d0c..ecd4c88ec4bf 100644
--- a/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.resolver.ts
+++ b/apps/judicial-system/api/src/app/modules/file/limitedAccessFile.resolver.ts
@@ -84,14 +84,18 @@ export class LimitedAccessFileResolver {
@Context('dataSources')
{ backendService }: { backendService: BackendService },
): Promise {
- const { caseId, id } = input
+ const { caseId, id, mergedCaseId } = input
this.logger.debug(`Getting a signed url for file ${id} of case ${caseId}`)
return this.auditTrailService.audit(
user.id,
AuditedAction.GET_SIGNED_URL,
- backendService.limitedAccessGetCaseFileSignedUrl(caseId, id),
+ backendService.limitedAccessGetCaseFileSignedUrl(
+ caseId,
+ id,
+ mergedCaseId,
+ ),
id,
)
}
diff --git a/apps/judicial-system/backend/src/app/modules/aws-s3/awsS3.service.ts b/apps/judicial-system/backend/src/app/modules/aws-s3/awsS3.service.ts
index 04c57cf5f55e..3ae180e14d27 100644
--- a/apps/judicial-system/backend/src/app/modules/aws-s3/awsS3.service.ts
+++ b/apps/judicial-system/backend/src/app/modules/aws-s3/awsS3.service.ts
@@ -125,13 +125,18 @@ export class AwsS3Service {
caseType: CaseType,
key?: string,
timeToLive?: number,
+ useFreshSession = false,
): Promise {
if (!key) {
throw new Error('Key is required')
}
return new Promise((resolve, reject) => {
- this.s3.getSignedUrl(
+ const s3 = useFreshSession
+ ? new S3({ region: this.config.region })
+ : this.s3
+
+ s3.getSignedUrl(
'getObject',
{
Bucket: this.config.bucket,
@@ -155,6 +160,7 @@ export class AwsS3Service {
force: boolean,
confirmContent: (content: Buffer) => Promise,
timeToLive?: number,
+ useFreshSession = false,
): Promise {
if (!key) {
throw new Error('Key is required')
@@ -167,7 +173,12 @@ export class AwsS3Service {
const confirmedKey = formatConfirmedIndictmentCaseKey(key)
if (!force && (await this.objectExists(caseType, confirmedKey))) {
- return this.getSignedUrl(caseType, confirmedKey, timeToLive)
+ return this.getSignedUrl(
+ caseType,
+ confirmedKey,
+ timeToLive,
+ useFreshSession,
+ )
}
const confirmedContent = await this.getObject(caseType, key).then(
@@ -175,11 +186,11 @@ export class AwsS3Service {
)
if (!confirmedContent) {
- return this.getSignedUrl(caseType, key, timeToLive)
+ return this.getSignedUrl(caseType, key, timeToLive, useFreshSession)
}
return this.putObject(caseType, confirmedKey, confirmedContent).then(() =>
- this.getSignedUrl(caseType, confirmedKey, timeToLive),
+ this.getSignedUrl(caseType, confirmedKey, timeToLive, useFreshSession),
)
}
diff --git a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts
index 9efb4338c21f..084aefd70922 100644
--- a/apps/judicial-system/backend/src/app/modules/case/case.controller.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/case.controller.ts
@@ -79,6 +79,7 @@ import { CaseExistsGuard } from './guards/caseExists.guard'
import { CaseReadGuard } from './guards/caseRead.guard'
import { CaseTypeGuard } from './guards/caseType.guard'
import { CaseWriteGuard } from './guards/caseWrite.guard'
+import { MergedCaseExistsGuard } from './guards/mergedCaseExists.guard'
import {
courtOfAppealsAssistantTransitionRule,
courtOfAppealsAssistantUpdateRule,
@@ -549,6 +550,7 @@ export class CaseController {
CaseExistsGuard,
new CaseTypeGuard(indictmentCases),
CaseReadGuard,
+ MergedCaseExistsGuard,
)
@RolesRules(
prosecutorRule,
@@ -558,7 +560,10 @@ export class CaseController {
districtCourtRegistrarRule,
districtCourtAssistantRule,
)
- @Get('case/:caseId/caseFilesRecord/:policeCaseNumber')
+ @Get([
+ 'case/:caseId/caseFilesRecord/:policeCaseNumber',
+ 'case/:caseId/mergedCase/:mergedCaseId/caseFilesRecord/:policeCaseNumber',
+ ])
@ApiOkResponse({
content: { 'application/pdf': {} },
description:
@@ -705,6 +710,7 @@ export class CaseController {
CaseExistsGuard,
new CaseTypeGuard(indictmentCases),
CaseReadGuard,
+ MergedCaseExistsGuard,
)
@RolesRules(
prosecutorRule,
@@ -714,7 +720,10 @@ export class CaseController {
districtCourtRegistrarRule,
districtCourtAssistantRule,
)
- @Get('case/:caseId/indictment')
+ @Get([
+ 'case/:caseId/indictment',
+ 'case/:caseId/mergedCase/:mergedCaseId/indictment',
+ ])
@Header('Content-Type', 'application/pdf')
@ApiOkResponse({
content: { 'application/pdf': {} },
diff --git a/apps/judicial-system/backend/src/app/modules/case/case.service.ts b/apps/judicial-system/backend/src/app/modules/case/case.service.ts
index 7361bc67846f..60896a1422b8 100644
--- a/apps/judicial-system/backend/src/app/modules/case/case.service.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/case.service.ts
@@ -321,6 +321,10 @@ export const include: Includeable[] = [
CaseFileCategory.CRIMINAL_RECORD,
CaseFileCategory.COST_BREAKDOWN,
CaseFileCategory.CRIMINAL_RECORD_UPDATE,
+ CaseFileCategory.CASE_FILE,
+ CaseFileCategory.PROSECUTOR_CASE_FILE,
+ CaseFileCategory.DEFENDANT_CASE_FILE,
+ CaseFileCategory.CIVIL_CLAIM,
],
},
},
@@ -337,10 +341,10 @@ export const include: Includeable[] = [
export const order: OrderItem[] = [
[{ model: Defendant, as: 'defendants' }, 'created', 'ASC'],
+ [{ model: CivilClaimant, as: 'civilClaimants' }, 'created', 'ASC'],
[{ model: IndictmentCount, as: 'indictmentCounts' }, 'created', 'ASC'],
[{ model: DateLog, as: 'dateLogs' }, 'created', 'DESC'],
[{ model: Notification, as: 'notifications' }, 'created', 'DESC'],
- [{ model: CivilClaimant, as: 'civilClaimants' }, 'created', 'ASC'],
]
export const caseListInclude: Includeable[] = [
@@ -388,14 +392,13 @@ export const caseListInclude: Includeable[] = [
as: 'eventLogs',
required: false,
where: { eventType: { [Op.in]: eventTypes } },
- order: [['created', 'DESC']],
- separate: true,
},
]
export const listOrder: OrderItem[] = [
[{ model: Defendant, as: 'defendants' }, 'created', 'ASC'],
[{ model: DateLog, as: 'dateLogs' }, 'created', 'DESC'],
+ [{ model: EventLog, as: 'eventLogs' }, 'created', 'DESC'],
]
@Injectable()
diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts b/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts
index dfb9f7bc678f..59506016d28b 100644
--- a/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts
@@ -6,6 +6,7 @@ import {
CaseIndictmentRulingDecision,
CaseState,
CaseType,
+ EventType,
getIndictmentVerdictAppealDeadlineStatus,
IndictmentCaseReviewDecision,
isCourtOfAppealsUser,
@@ -87,6 +88,27 @@ const canPublicProsecutionUserAccessCase = (theCase: Case): boolean => {
return false
}
+ // Check indictment ruling decision access
+ if (
+ !theCase.indictmentRulingDecision ||
+ ![
+ CaseIndictmentRulingDecision.FINE,
+ CaseIndictmentRulingDecision.RULING,
+ ].includes(theCase.indictmentRulingDecision)
+ ) {
+ return false
+ }
+
+ // Make sure the indictment has been sent to the public prosecutor
+ if (
+ !theCase.eventLogs?.some(
+ (eventLog) =>
+ eventLog.eventType === EventType.INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR,
+ )
+ ) {
+ return false
+ }
+
return true
}
diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts b/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts
index 0ccb2aa3c953..c0df2270d241 100644
--- a/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts
@@ -11,6 +11,7 @@ import {
CaseState,
CaseType,
DateType,
+ EventType,
IndictmentCaseReviewDecision,
indictmentCases,
investigationCases,
@@ -75,8 +76,20 @@ const getPublicProsecutionUserCasesQueryFilter = (): WhereOptions => {
return {
[Op.and]: [
{ is_archived: false },
- { state: [CaseState.COMPLETED] },
{ type: indictmentCases },
+ { state: CaseState.COMPLETED },
+ {
+ indictment_ruling_decision: [
+ CaseIndictmentRulingDecision.FINE,
+ CaseIndictmentRulingDecision.RULING,
+ ],
+ },
+ {
+ // The following condition will filter out all event logs that are not of type INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR
+ // but that should be ok the case list for the public prosecutor is not using other event logs
+ '$eventLogs.event_type$':
+ EventType.INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR,
+ },
],
}
}
@@ -190,15 +203,10 @@ const getPrisonAdminUserCasesQueryFilter = (): WhereOptions => {
[Op.or]: [
{
state: CaseState.ACCEPTED,
- type: [
- CaseType.CUSTODY,
- CaseType.ADMISSION_TO_FACILITY,
- CaseType.PAROLE_REVOCATION,
- CaseType.TRAVEL_BAN,
- ],
+ type: [...restrictionCases, CaseType.PAROLE_REVOCATION],
},
{
- type: CaseType.INDICTMENT,
+ type: indictmentCases,
state: CaseState.COMPLETED,
indictment_ruling_decision: CaseIndictmentRulingDecision.RULING,
indictment_review_decision: IndictmentCaseReviewDecision.ACCEPT,
diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts b/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts
index 0e2217081425..65c759346088 100644
--- a/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts
@@ -12,6 +12,7 @@ import {
courtOfAppealsRoles,
DateType,
districtCourtRoles,
+ EventType,
IndictmentCaseReviewDecision,
indictmentCases,
InstitutionType,
@@ -306,11 +307,17 @@ describe('getCasesQueryFilter', () => {
expect(res).toStrictEqual({
[Op.and]: [
{ is_archived: false },
+ { type: indictmentCases },
+ { state: CaseState.COMPLETED },
{
- state: [CaseState.COMPLETED],
+ indictment_ruling_decision: [
+ CaseIndictmentRulingDecision.FINE,
+ CaseIndictmentRulingDecision.RULING,
+ ],
},
{
- type: indictmentCases,
+ '$eventLogs.event_type$':
+ EventType.INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR,
},
],
})
@@ -371,15 +378,10 @@ describe('getCasesQueryFilter', () => {
[Op.or]: [
{
state: CaseState.ACCEPTED,
- type: [
- CaseType.CUSTODY,
- CaseType.ADMISSION_TO_FACILITY,
- CaseType.PAROLE_REVOCATION,
- CaseType.TRAVEL_BAN,
- ],
+ type: [...restrictionCases, CaseType.PAROLE_REVOCATION],
},
{
- type: CaseType.INDICTMENT,
+ type: indictmentCases,
state: CaseState.COMPLETED,
indictment_ruling_decision: CaseIndictmentRulingDecision.RULING,
indictment_review_decision: IndictmentCaseReviewDecision.ACCEPT,
diff --git a/apps/judicial-system/backend/src/app/modules/case/guards/mergedCaseExists.guard.ts b/apps/judicial-system/backend/src/app/modules/case/guards/mergedCaseExists.guard.ts
new file mode 100644
index 000000000000..2347b578d010
--- /dev/null
+++ b/apps/judicial-system/backend/src/app/modules/case/guards/mergedCaseExists.guard.ts
@@ -0,0 +1,45 @@
+import {
+ BadRequestException,
+ CanActivate,
+ ExecutionContext,
+ Injectable,
+ InternalServerErrorException,
+} from '@nestjs/common'
+
+import { CaseService } from '../case.service'
+import { Case } from '../models/case.model'
+
+@Injectable()
+export class MergedCaseExistsGuard implements CanActivate {
+ constructor(private readonly caseService: CaseService) {}
+
+ async canActivate(context: ExecutionContext): Promise {
+ const request = context.switchToHttp().getRequest()
+
+ const mergedCaseId = request.params.mergedCaseId
+
+ // If the user is not accessing a merged case, we don't need to do anything
+ if (!mergedCaseId) {
+ return true
+ }
+
+ const theCase: Case = request.case
+
+ if (!theCase) {
+ throw new InternalServerErrorException('Missing case')
+ }
+
+ const mergedCase = theCase.mergedCases?.find(
+ (mergedCase) => mergedCase.id === mergedCaseId,
+ )
+
+ if (!mergedCase) {
+ throw new BadRequestException('Merged case not found')
+ }
+
+ request.params.caseId = mergedCaseId
+ request.case = mergedCase
+
+ return true
+ }
+}
diff --git a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts
index ae2fb90f556e..4ef8b7b5a247 100644
--- a/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/internalCase.service.ts
@@ -58,6 +58,7 @@ import { CaseFile, FileService } from '../file'
import { IndictmentCount, IndictmentCountService } from '../indictment-count'
import { Institution } from '../institution'
import { PoliceDocument, PoliceDocumentType, PoliceService } from '../police'
+import { Subpoena } from '../subpoena/models/subpoena.model'
import { User, UserService } from '../user'
import { InternalCreateCaseDto } from './dto/internalCreateCase.dto'
import { archiveFilter } from './filters/case.archiveFilter'
@@ -1204,7 +1205,10 @@ export class InternalCaseService {
async getIndictmentCases(nationalId: string): Promise {
return this.caseModel.findAll({
include: [
- { model: Defendant, as: 'defendants' },
+ {
+ model: Defendant,
+ as: 'defendants',
+ },
{
model: DateLog,
as: 'dateLogs',
@@ -1228,11 +1232,25 @@ export class InternalCaseService {
async getIndictmentCase(caseId: string, nationalId: string): Promise {
const caseById = await this.caseModel.findOne({
include: [
- { model: Defendant, as: 'defendants' },
+ {
+ model: Defendant,
+ as: 'defendants',
+ include: [
+ {
+ model: Subpoena,
+ as: 'subpoenas',
+ order: [['created', 'DESC']],
+ },
+ ],
+ },
{ model: Institution, as: 'court' },
{ model: Institution, as: 'prosecutorsOffice' },
{ model: User, as: 'judge' },
- { model: User, as: 'prosecutor' },
+ {
+ model: User,
+ as: 'prosecutor',
+ include: [{ model: Institution, as: 'institution' }],
+ },
{ model: DateLog, as: 'dateLogs' },
],
attributes: ['courtCaseNumber', 'id'],
diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts
index 4663b613d9a8..aa1a56f603ae 100644
--- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.controller.ts
@@ -51,6 +51,7 @@ import { CaseReadGuard } from './guards/caseRead.guard'
import { CaseTypeGuard } from './guards/caseType.guard'
import { CaseWriteGuard } from './guards/caseWrite.guard'
import { LimitedAccessCaseExistsGuard } from './guards/limitedAccessCaseExists.guard'
+import { MergedCaseExistsGuard } from './guards/mergedCaseExists.guard'
import { RequestSharedWithDefenderGuard } from './guards/requestSharedWithDefender.guard'
import { defenderTransitionRule, defenderUpdateRule } from './guards/rolesRules'
import { CaseInterceptor } from './interceptors/case.interceptor'
@@ -71,7 +72,6 @@ export class LimitedAccessCaseController {
private readonly limitedAccessCaseService: LimitedAccessCaseService,
private readonly eventService: EventService,
private readonly pdfService: PdfService,
-
@Inject(LOGGER_PROVIDER) private readonly logger: Logger,
) {}
@@ -244,9 +244,13 @@ export class LimitedAccessCaseController {
CaseExistsGuard,
new CaseTypeGuard(indictmentCases),
CaseReadGuard,
+ MergedCaseExistsGuard,
)
@RolesRules(defenderRule)
- @Get('case/:caseId/limitedAccess/caseFilesRecord/:policeCaseNumber')
+ @Get([
+ 'case/:caseId/limitedAccess/caseFilesRecord/:policeCaseNumber',
+ 'case/:caseId/limitedAccess/mergedCase/:mergedCaseId/caseFilesRecord/:policeCaseNumber',
+ ])
@ApiOkResponse({
content: { 'application/pdf': {} },
description:
@@ -375,9 +379,13 @@ export class LimitedAccessCaseController {
CaseExistsGuard,
new CaseTypeGuard(indictmentCases),
CaseReadGuard,
+ MergedCaseExistsGuard,
)
@RolesRules(defenderRule)
- @Get('case/:caseId/limitedAccess/indictment')
+ @Get([
+ 'case/:caseId/limitedAccess/indictment',
+ 'case/:caseId/limitedAccess/mergedCase/:mergedCaseId/indictment',
+ ])
@Header('Content-Type', 'application/pdf')
@ApiOkResponse({
content: { 'application/pdf': {} },
diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts
index 1f9b75a06b9f..fa2e9b5bbaeb 100644
--- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts
@@ -30,12 +30,13 @@ import {
import { nowFactory, uuidFactory } from '../../factories'
import { AwsS3Service } from '../aws-s3'
-import { Defendant, DefendantService } from '../defendant'
+import { CivilClaimant, Defendant, DefendantService } from '../defendant'
import { EventLog } from '../event-log'
import {
CaseFile,
defenderCaseFileCategoriesForRestrictionAndInvestigationCases,
} from '../file'
+import { IndictmentCount } from '../indictment-count'
import { Institution } from '../institution'
import { User } from '../user'
import { Case } from './models/case.model'
@@ -102,6 +103,7 @@ export const attributes: (keyof Case)[] = [
'courtSessionType',
'indictmentReviewDecision',
'indictmentReviewerId',
+ 'hasCivilClaims',
]
export interface LimitedAccessUpdateCase
@@ -169,6 +171,8 @@ export const include: Includeable[] = [
{ model: Case, as: 'parentCase', attributes },
{ model: Case, as: 'childCase', attributes },
{ model: Defendant, as: 'defendants' },
+ { model: IndictmentCount, as: 'indictmentCounts' },
+ { model: CivilClaimant, as: 'civilClaimants' },
{
model: CaseFile,
as: 'caseFiles',
@@ -218,10 +222,42 @@ export const include: Includeable[] = [
where: { stringType: { [Op.in]: stringTypes } },
},
{ model: Case, as: 'mergeCase', attributes },
+ {
+ model: Case,
+ as: 'mergedCases',
+ where: { state: CaseState.COMPLETED },
+ include: [
+ {
+ model: CaseFile,
+ as: 'caseFiles',
+ required: false,
+ where: {
+ state: { [Op.not]: CaseFileState.DELETED },
+ category: {
+ [Op.in]: [
+ CaseFileCategory.INDICTMENT,
+ CaseFileCategory.COURT_RECORD,
+ CaseFileCategory.CRIMINAL_RECORD,
+ CaseFileCategory.COST_BREAKDOWN,
+ CaseFileCategory.CRIMINAL_RECORD_UPDATE,
+ CaseFileCategory.CASE_FILE,
+ CaseFileCategory.PROSECUTOR_CASE_FILE,
+ CaseFileCategory.DEFENDANT_CASE_FILE,
+ CaseFileCategory.CIVIL_CLAIM,
+ ],
+ },
+ },
+ separate: true,
+ },
+ ],
+ separate: true,
+ },
]
export const order: OrderItem[] = [
[{ model: Defendant, as: 'defendants' }, 'created', 'ASC'],
+ [{ model: IndictmentCount, as: 'indictmentCounts' }, 'created', 'ASC'],
+ [{ model: CivilClaimant, as: 'civilClaimants' }, 'created', 'ASC'],
[{ model: DateLog, as: 'dateLogs' }, 'created', 'DESC'],
]
diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfGuards.spec.ts
index f59fec872cbf..1647704e5a27 100644
--- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfGuards.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getCaseFilesRecordPdfGuards.spec.ts
@@ -5,6 +5,7 @@ import { CaseController } from '../../case.controller'
import { CaseExistsGuard } from '../../guards/caseExists.guard'
import { CaseReadGuard } from '../../guards/caseRead.guard'
import { CaseTypeGuard } from '../../guards/caseType.guard'
+import { MergedCaseExistsGuard } from '../../guards/mergedCaseExists.guard'
describe('CaseController - Get case files record pdf guards', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -18,7 +19,7 @@ describe('CaseController - Get case files record pdf guards', () => {
})
it('should have the right guard configuration', () => {
- expect(guards).toHaveLength(5)
+ expect(guards).toHaveLength(6)
expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard)
expect(new guards[1]()).toBeInstanceOf(RolesGuard)
expect(new guards[2]()).toBeInstanceOf(CaseExistsGuard)
@@ -27,5 +28,6 @@ describe('CaseController - Get case files record pdf guards', () => {
allowedCaseTypes: indictmentCases,
})
expect(new guards[4]()).toBeInstanceOf(CaseReadGuard)
+ expect(new guards[5]()).toBeInstanceOf(MergedCaseExistsGuard)
})
})
diff --git a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfGuards.spec.ts
index fa644b448dc1..d86a9827ce80 100644
--- a/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfGuards.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/test/caseController/getIndictmentPdfGuards.spec.ts
@@ -7,6 +7,7 @@ import { CaseController } from '../../case.controller'
import { CaseExistsGuard } from '../../guards/caseExists.guard'
import { CaseReadGuard } from '../../guards/caseRead.guard'
import { CaseTypeGuard } from '../../guards/caseType.guard'
+import { MergedCaseExistsGuard } from '../../guards/mergedCaseExists.guard'
describe('CaseController - Get indictment pdf guards', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -19,70 +20,16 @@ describe('CaseController - Get indictment pdf guards', () => {
)
})
- it('should have five guards', () => {
- expect(guards).toHaveLength(5)
- })
-
- describe('JwtAuthGuard', () => {
- let guard: CanActivate
-
- beforeEach(() => {
- guard = new guards[0]()
- })
-
- it('should have JwtAuthGuard as guard 1', () => {
- expect(guard).toBeInstanceOf(JwtAuthGuard)
- })
- })
-
- describe('RolesGuard', () => {
- let guard: CanActivate
-
- beforeEach(() => {
- guard = new guards[1]()
- })
-
- it('should have RolesGuard as guard 2', () => {
- expect(guard).toBeInstanceOf(RolesGuard)
- })
- })
-
- describe('CaseExistsGuard', () => {
- let guard: CanActivate
-
- beforeEach(() => {
- guard = new guards[2]()
- })
-
- it('should have CaseExistsGuard as guard 3', () => {
- expect(guard).toBeInstanceOf(CaseExistsGuard)
- })
- })
-
- describe('CaseTypeGuard', () => {
- let guard: CanActivate
-
- beforeEach(() => {
- guard = guards[3]
- })
-
- it('should have CaseTypeGuard as guard 4', () => {
- expect(guard).toBeInstanceOf(CaseTypeGuard)
- expect(guard).toEqual({
- allowedCaseTypes: indictmentCases,
- })
- })
- })
-
- describe('CaseReadGuard', () => {
- let guard: CanActivate
-
- beforeEach(() => {
- guard = new guards[4]()
- })
-
- it('should have CaseReadGuard as guard 5', () => {
- expect(guard).toBeInstanceOf(CaseReadGuard)
- })
+ it('should have the right guard configuration', () => {
+ expect(guards).toHaveLength(6)
+ expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard)
+ expect(new guards[1]()).toBeInstanceOf(RolesGuard)
+ expect(new guards[2]()).toBeInstanceOf(CaseExistsGuard)
+ expect(guards[3]).toBeInstanceOf(CaseTypeGuard)
+ expect(guards[3]).toEqual({
+ allowedCaseTypes: indictmentCases,
+ })
+ expect(new guards[4]()).toBeInstanceOf(CaseReadGuard)
+ expect(new guards[5]()).toBeInstanceOf(MergedCaseExistsGuard)
})
})
diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfGuards.spec.ts
index 837fb66529ea..f9cf460fb962 100644
--- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfGuards.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getCaseFilesRecordPdfGuards.spec.ts
@@ -4,6 +4,7 @@ import { indictmentCases } from '@island.is/judicial-system/types'
import { CaseExistsGuard } from '../../guards/caseExists.guard'
import { CaseReadGuard } from '../../guards/caseRead.guard'
import { CaseTypeGuard } from '../../guards/caseType.guard'
+import { MergedCaseExistsGuard } from '../../guards/mergedCaseExists.guard'
import { LimitedAccessCaseController } from '../../limitedAccessCase.controller'
describe('LimitedAccessCaseController - Get case files record pdf guards', () => {
@@ -18,7 +19,7 @@ describe('LimitedAccessCaseController - Get case files record pdf guards', () =>
})
it('should have the right guard configuration', () => {
- expect(guards).toHaveLength(5)
+ expect(guards).toHaveLength(6)
expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard)
expect(new guards[1]()).toBeInstanceOf(RolesGuard)
expect(new guards[2]()).toBeInstanceOf(CaseExistsGuard)
@@ -27,5 +28,6 @@ describe('LimitedAccessCaseController - Get case files record pdf guards', () =>
allowedCaseTypes: indictmentCases,
})
expect(new guards[4]()).toBeInstanceOf(CaseReadGuard)
+ expect(new guards[5]()).toBeInstanceOf(MergedCaseExistsGuard)
})
})
diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfGuards.spec.ts
index 3b375c1ce78d..bc2a8f2e1546 100644
--- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfGuards.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/getIndictmentPdfGuards.spec.ts
@@ -4,6 +4,7 @@ import { indictmentCases } from '@island.is/judicial-system/types'
import { CaseExistsGuard } from '../../guards/caseExists.guard'
import { CaseReadGuard } from '../../guards/caseRead.guard'
import { CaseTypeGuard } from '../../guards/caseType.guard'
+import { MergedCaseExistsGuard } from '../../guards/mergedCaseExists.guard'
import { LimitedAccessCaseController } from '../../limitedAccessCase.controller'
describe('LimitedAccessCaseController - Get indictment pdf guards', () => {
@@ -18,7 +19,7 @@ describe('LimitedAccessCaseController - Get indictment pdf guards', () => {
})
it('should have the right guard configuration', () => {
- expect(guards).toHaveLength(5)
+ expect(guards).toHaveLength(6)
expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard)
expect(new guards[1]()).toBeInstanceOf(RolesGuard)
expect(new guards[2]()).toBeInstanceOf(CaseExistsGuard)
@@ -27,5 +28,6 @@ describe('LimitedAccessCaseController - Get indictment pdf guards', () => {
allowedCaseTypes: indictmentCases,
})
expect(new guards[4]()).toBeInstanceOf(CaseReadGuard)
+ expect(new guards[5]()).toBeInstanceOf(MergedCaseExistsGuard)
})
})
diff --git a/apps/judicial-system/backend/src/app/modules/court/court.service.ts b/apps/judicial-system/backend/src/app/modules/court/court.service.ts
index 78d154cc509b..208ff6752f6b 100644
--- a/apps/judicial-system/backend/src/app/modules/court/court.service.ts
+++ b/apps/judicial-system/backend/src/app/modules/court/court.service.ts
@@ -1,4 +1,5 @@
import formatISO from 'date-fns/formatISO'
+import { Base64 } from 'js-base64'
import { Sequelize } from 'sequelize-typescript'
import { ConfidentialClientApplication } from '@azure/msal-node'
@@ -840,7 +841,12 @@ export class CourtService {
): Promise {
try {
const subject = `Landsréttur - ${appealCaseNumber} - skjal`
- const content = JSON.stringify({ category, name, dateSent, url })
+ const content = JSON.stringify({
+ category,
+ name,
+ dateSent,
+ url: url && Base64.encode(url),
+ })
return this.sendToRobot(
subject,
diff --git a/apps/judicial-system/backend/src/app/modules/defendant/defendant.controller.ts b/apps/judicial-system/backend/src/app/modules/defendant/defendant.controller.ts
index 5905ae989067..cb7ab62524ed 100644
--- a/apps/judicial-system/backend/src/app/modules/defendant/defendant.controller.ts
+++ b/apps/judicial-system/backend/src/app/modules/defendant/defendant.controller.ts
@@ -156,6 +156,9 @@ export class DefendantController {
DefendantExistsGuard,
)
@RolesRules(
+ prosecutorRule,
+ prosecutorRepresentativeRule,
+ publicProsecutorStaffRule,
districtCourtJudgeRule,
districtCourtRegistrarRule,
districtCourtAssistantRule,
diff --git a/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts b/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts
index a7cb4fca9e0f..7fc42f0f16fa 100644
--- a/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts
+++ b/apps/judicial-system/backend/src/app/modules/defendant/defendant.module.ts
@@ -5,6 +5,7 @@ import { MessageModule } from '@island.is/judicial-system/message'
import { CaseModule } from '../case/case.module'
import { CourtModule } from '../court/court.module'
+import { Subpoena } from '../subpoena/models/subpoena.model'
import { CivilClaimant } from './models/civilClaimant.model'
import { Defendant } from './models/defendant.model'
import { CivilClaimantController } from './civilClaimant.controller'
@@ -12,17 +13,19 @@ import { CivilClaimantService } from './civilClaimant.service'
import { DefendantController } from './defendant.controller'
import { DefendantService } from './defendant.service'
import { InternalDefendantController } from './internalDefendant.controller'
+import { LimitedAccessDefendantController } from './limitedAccessDefendant.controller'
@Module({
imports: [
MessageModule,
forwardRef(() => CourtModule),
forwardRef(() => CaseModule),
- SequelizeModule.forFeature([Defendant, CivilClaimant]),
+ SequelizeModule.forFeature([Defendant, CivilClaimant, Subpoena]),
],
controllers: [
DefendantController,
InternalDefendantController,
+ LimitedAccessDefendantController,
CivilClaimantController,
],
providers: [DefendantService, CivilClaimantService],
diff --git a/apps/judicial-system/backend/src/app/modules/defendant/limitedAccessDefendant.controller.ts b/apps/judicial-system/backend/src/app/modules/defendant/limitedAccessDefendant.controller.ts
new file mode 100644
index 000000000000..d2e79ab2ef40
--- /dev/null
+++ b/apps/judicial-system/backend/src/app/modules/defendant/limitedAccessDefendant.controller.ts
@@ -0,0 +1,85 @@
+import { Response } from 'express'
+
+import {
+ Controller,
+ Get,
+ Header,
+ Inject,
+ Param,
+ Query,
+ Res,
+ UseGuards,
+} from '@nestjs/common'
+import { ApiOkResponse, ApiTags } from '@nestjs/swagger'
+
+import type { Logger } from '@island.is/logging'
+import { LOGGER_PROVIDER } from '@island.is/logging'
+
+import {
+ JwtAuthGuard,
+ RolesGuard,
+ RolesRules,
+} from '@island.is/judicial-system/auth'
+import { indictmentCases, SubpoenaType } from '@island.is/judicial-system/types'
+
+import { defenderRule } from '../../guards'
+import {
+ Case,
+ CaseExistsGuard,
+ CaseReadGuard,
+ CaseTypeGuard,
+ CurrentCase,
+ PdfService,
+} from '../case'
+import { CurrentDefendant } from './guards/defendant.decorator'
+import { DefendantExistsGuard } from './guards/defendantExists.guard'
+import { Defendant } from './models/defendant.model'
+
+@Controller('api/case/:caseId/limitedAccess/defendant/:defendantId/subpoena')
+@UseGuards(
+ JwtAuthGuard,
+ RolesGuard,
+ CaseExistsGuard,
+ new CaseTypeGuard(indictmentCases),
+ CaseReadGuard,
+ DefendantExistsGuard,
+)
+@ApiTags('limited access defendants')
+export class LimitedAccessDefendantController {
+ constructor(
+ private readonly pdfService: PdfService,
+ @Inject(LOGGER_PROVIDER) private readonly logger: Logger,
+ ) {}
+
+ @RolesRules(defenderRule)
+ @Get()
+ @Header('Content-Type', 'application/pdf')
+ @ApiOkResponse({
+ content: { 'application/pdf': {} },
+ description: 'Gets the subpoena for a given defendant as a pdf document',
+ })
+ async getSubpoenaPdf(
+ @Param('caseId') caseId: string,
+ @Param('defendantId') defendantId: string,
+ @CurrentCase() theCase: Case,
+ @CurrentDefendant() defendant: Defendant,
+ @Res() res: Response,
+ @Query('arraignmentDate') arraignmentDate?: Date,
+ @Query('location') location?: string,
+ @Query('subpoenaType') subpoenaType?: SubpoenaType,
+ ): Promise {
+ this.logger.debug(
+ `Getting the subpoena for defendant ${defendantId} of case ${caseId} as a pdf document`,
+ )
+
+ const pdf = await this.pdfService.getSubpoenaPdf(
+ theCase,
+ defendant,
+ arraignmentDate,
+ location,
+ subpoenaType,
+ )
+
+ res.end(pdf)
+ }
+}
diff --git a/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts b/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts
index 093344f50641..a72b69edeed0 100644
--- a/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts
+++ b/apps/judicial-system/backend/src/app/modules/defendant/models/defendant.model.ts
@@ -4,6 +4,7 @@ import {
CreatedAt,
DataType,
ForeignKey,
+ HasMany,
Model,
Table,
UpdatedAt,
@@ -20,6 +21,7 @@ import {
} from '@island.is/judicial-system/types'
import { Case } from '../../case/models/case.model'
+import { Subpoena } from '../../subpoena/models/subpoena.model'
@Table({
tableName: 'defendant',
@@ -131,4 +133,8 @@ export class Defendant extends Model {
})
@ApiPropertyOptional({ enum: SubpoenaType })
subpoenaType?: SubpoenaType
+
+ @HasMany(() => Subpoena, { foreignKey: 'defendantId' })
+ @ApiPropertyOptional({ type: () => Subpoena, isArray: true })
+ subpoenas?: Subpoena[]
}
diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts
index 345415e2fcfe..8cb18eb62706 100644
--- a/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts
+++ b/apps/judicial-system/backend/src/app/modules/defendant/test/createTestingDefendantModule.ts
@@ -16,6 +16,7 @@ import { UserService } from '../../user'
import { DefendantController } from '../defendant.controller'
import { DefendantService } from '../defendant.service'
import { InternalDefendantController } from '../internalDefendant.controller'
+import { LimitedAccessDefendantController } from '../limitedAccessDefendant.controller'
import { Defendant } from '../models/defendant.model'
jest.mock('@island.is/judicial-system/message')
@@ -27,7 +28,11 @@ jest.mock('../../case/pdf.service')
export const createTestingDefendantModule = async () => {
const defendantModule = await Test.createTestingModule({
imports: [ConfigModule.forRoot({ load: [sharedAuthModuleConfig] })],
- controllers: [DefendantController, InternalDefendantController],
+ controllers: [
+ DefendantController,
+ InternalDefendantController,
+ LimitedAccessDefendantController,
+ ],
providers: [
SharedAuthModule,
MessageService,
@@ -81,6 +86,11 @@ export const createTestingDefendantModule = async () => {
InternalDefendantController,
)
+ const limitedAccessDefendantController =
+ defendantModule.get(
+ LimitedAccessDefendantController,
+ )
+
defendantModule.close()
return {
@@ -92,5 +102,6 @@ export const createTestingDefendantModule = async () => {
defendantService,
defendantController,
internalDefendantController,
+ limitedAccessDefendantController,
}
}
diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/defendantControllerGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/defendantControllerGuards.spec.ts
index 0e8128581aaa..a7483b52a5a3 100644
--- a/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/defendantControllerGuards.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/defendantControllerGuards.spec.ts
@@ -1,5 +1,3 @@
-import { CanActivate } from '@nestjs/common'
-
import { JwtAuthGuard, RolesGuard } from '@island.is/judicial-system/auth'
import { DefendantController } from '../../defendant.controller'
@@ -12,31 +10,9 @@ describe('DefendantController - guards', () => {
guards = Reflect.getMetadata('__guards__', DefendantController)
})
- it('should have two guards', () => {
+ it('should have the right guard configuration', () => {
expect(guards).toHaveLength(2)
- })
-
- describe('JwtAuthGuard', () => {
- let guard: CanActivate
-
- beforeEach(() => {
- guard = new guards[0]()
- })
-
- it('should have JwtAuthGuard as guard 1', () => {
- expect(guard).toBeInstanceOf(JwtAuthGuard)
- })
- })
-
- describe('RolesGuard', () => {
- let guard: CanActivate
-
- beforeEach(() => {
- guard = new guards[1]()
- })
-
- it('should have RolesGuard as guard 2', () => {
- expect(guard).toBeInstanceOf(RolesGuard)
- })
+ expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard)
+ expect(new guards[1]()).toBeInstanceOf(RolesGuard)
})
})
diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/getSubpoenaPdfGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/getSubpoenaPdfGuards.spec.ts
index e524eadbe265..7fb9d19876bb 100644
--- a/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/getSubpoenaPdfGuards.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/getSubpoenaPdfGuards.spec.ts
@@ -4,7 +4,7 @@ import { CaseExistsGuard, CaseReadGuard, CaseTypeGuard } from '../../../case'
import { DefendantController } from '../../defendant.controller'
import { DefendantExistsGuard } from '../../guards/defendantExists.guard'
-describe('CaseController - Get custody notice pdf guards', () => {
+describe('DefendantController - Get custody notice pdf guards', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let guards: any[]
diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/getSubpoenaPdfRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/getSubpoenaPdfRolesRules.spec.ts
index b40e86c6872b..286050136fcd 100644
--- a/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/getSubpoenaPdfRolesRules.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/defendant/test/defendantController/getSubpoenaPdfRolesRules.spec.ts
@@ -2,10 +2,13 @@ import {
districtCourtAssistantRule,
districtCourtJudgeRule,
districtCourtRegistrarRule,
+ prosecutorRepresentativeRule,
+ prosecutorRule,
+ publicProsecutorStaffRule,
} from '../../../../guards'
import { DefendantController } from '../../defendant.controller'
-describe('CaseController - Get custody notice pdf rules', () => {
+describe('DefendantController - Get custody notice pdf rules', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let rules: any[]
@@ -17,7 +20,10 @@ describe('CaseController - Get custody notice pdf rules', () => {
})
it('should give permission to roles', () => {
- expect(rules).toHaveLength(3)
+ expect(rules).toHaveLength(6)
+ expect(rules).toContain(prosecutorRule)
+ expect(rules).toContain(prosecutorRepresentativeRule)
+ expect(rules).toContain(publicProsecutorStaffRule)
expect(rules).toContain(districtCourtJudgeRule)
expect(rules).toContain(districtCourtRegistrarRule)
expect(rules).toContain(districtCourtAssistantRule)
diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/limitedAccessDefendantController/getSubpoenaPdf.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/limitedAccessDefendantController/getSubpoenaPdf.spec.ts
new file mode 100644
index 000000000000..97ec46dde723
--- /dev/null
+++ b/apps/judicial-system/backend/src/app/modules/defendant/test/limitedAccessDefendantController/getSubpoenaPdf.spec.ts
@@ -0,0 +1,68 @@
+import { Response } from 'express'
+import { uuid } from 'uuidv4'
+
+import { createTestingDefendantModule } from '../createTestingDefendantModule'
+
+import { Case, PdfService } from '../../../case'
+import { Defendant } from '../../models/defendant.model'
+
+interface Then {
+ error: Error
+}
+
+type GivenWhenThen = () => Promise
+
+describe('LimitedAccessDefendantController - Get subpoena pdf', () => {
+ const caseId = uuid()
+ const defendantId = uuid()
+ const defendant = { id: defendantId } as Defendant
+ const theCase = { id: caseId } as Case
+ const res = { end: jest.fn() } as unknown as Response
+ const pdf = Buffer.from(uuid())
+ let mockPdfService: PdfService
+ let givenWhenThen: GivenWhenThen
+
+ beforeEach(async () => {
+ const { pdfService, limitedAccessDefendantController } =
+ await createTestingDefendantModule()
+
+ mockPdfService = pdfService
+ const getSubpoenaPdfMock = mockPdfService.getSubpoenaPdf as jest.Mock
+ getSubpoenaPdfMock.mockResolvedValueOnce(pdf)
+
+ givenWhenThen = async () => {
+ const then = {} as Then
+
+ try {
+ await limitedAccessDefendantController.getSubpoenaPdf(
+ caseId,
+ defendantId,
+ theCase,
+ defendant,
+ res,
+ )
+ } catch (error) {
+ then.error = error as Error
+ }
+
+ return then
+ }
+ })
+
+ describe('pdf generated', () => {
+ beforeEach(async () => {
+ await givenWhenThen()
+ })
+
+ it('should generate pdf', () => {
+ expect(mockPdfService.getSubpoenaPdf).toHaveBeenCalledWith(
+ theCase,
+ defendant,
+ undefined,
+ undefined,
+ undefined,
+ )
+ expect(res.end).toHaveBeenCalledWith(pdf)
+ })
+ })
+})
diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/limitedAccessDefendantController/getSubpoenaPdfRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/limitedAccessDefendantController/getSubpoenaPdfRolesRules.spec.ts
new file mode 100644
index 000000000000..87cbfde789f8
--- /dev/null
+++ b/apps/judicial-system/backend/src/app/modules/defendant/test/limitedAccessDefendantController/getSubpoenaPdfRolesRules.spec.ts
@@ -0,0 +1,19 @@
+import { defenderRule } from '../../../../guards'
+import { LimitedAccessDefendantController } from '../../limitedAccessDefendant.controller'
+
+describe('LimitedAccessDefendantController - Get custody notice pdf rules', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let rules: any[]
+
+ beforeEach(() => {
+ rules = Reflect.getMetadata(
+ 'roles-rules',
+ LimitedAccessDefendantController.prototype.getSubpoenaPdf,
+ )
+ })
+
+ it('should give permission to roles', () => {
+ expect(rules).toHaveLength(1)
+ expect(rules).toContain(defenderRule)
+ })
+})
diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/limitedAccessDefendantController/limitedAccessDefendantControllerGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/limitedAccessDefendantController/limitedAccessDefendantControllerGuards.spec.ts
new file mode 100644
index 000000000000..aee6fdebeb8c
--- /dev/null
+++ b/apps/judicial-system/backend/src/app/modules/defendant/test/limitedAccessDefendantController/limitedAccessDefendantControllerGuards.spec.ts
@@ -0,0 +1,28 @@
+import { JwtAuthGuard, RolesGuard } from '@island.is/judicial-system/auth'
+import { indictmentCases } from '@island.is/judicial-system/types'
+
+import { CaseExistsGuard, CaseReadGuard, CaseTypeGuard } from '../../../case'
+import { DefendantExistsGuard } from '../../guards/defendantExists.guard'
+import { LimitedAccessDefendantController } from '../../limitedAccessDefendant.controller'
+
+describe('LimitedAccessDefendantController - guards', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let guards: any[]
+
+ beforeEach(() => {
+ guards = Reflect.getMetadata('__guards__', LimitedAccessDefendantController)
+ })
+
+ it('should have the right guard configuration', () => {
+ expect(guards).toHaveLength(6)
+ expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard)
+ expect(new guards[1]()).toBeInstanceOf(RolesGuard)
+ expect(new guards[2]()).toBeInstanceOf(CaseExistsGuard)
+ expect(guards[3]).toBeInstanceOf(CaseTypeGuard)
+ expect(guards[3]).toEqual({
+ allowedCaseTypes: indictmentCases,
+ })
+ expect(new guards[4]()).toBeInstanceOf(CaseReadGuard)
+ expect(new guards[5]()).toBeInstanceOf(DefendantExistsGuard)
+ })
+})
diff --git a/apps/judicial-system/backend/src/app/modules/file/file.controller.ts b/apps/judicial-system/backend/src/app/modules/file/file.controller.ts
index 340d782f767b..abbd76f30659 100644
--- a/apps/judicial-system/backend/src/app/modules/file/file.controller.ts
+++ b/apps/judicial-system/backend/src/app/modules/file/file.controller.ts
@@ -49,6 +49,7 @@ import {
CaseWriteGuard,
CurrentCase,
} from '../case'
+import { MergedCaseExistsGuard } from '../case/guards/mergedCaseExists.guard'
import { CreateFileDto } from './dto/createFile.dto'
import { CreatePresignedPostDto } from './dto/createPresignedPost.dto'
import { UpdateFilesDto } from './dto/updateFile.dto'
@@ -128,6 +129,7 @@ export class FileController {
RolesGuard,
CaseExistsGuard,
CaseReadGuard,
+ MergedCaseExistsGuard,
CaseFileExistsGuard,
ViewCaseFileGuard,
)
@@ -143,7 +145,7 @@ export class FileController {
courtOfAppealsAssistantRule,
prisonSystemStaffRule,
)
- @Get('file/:fileId/url')
+ @Get(['file/:fileId/url', 'mergedCase/:mergedCaseId/file/:fileId/url'])
@ApiOkResponse({
type: SignedUrl,
description: 'Gets a signed url for a case file',
diff --git a/apps/judicial-system/backend/src/app/modules/file/file.service.ts b/apps/judicial-system/backend/src/app/modules/file/file.service.ts
index 3bda7c821d48..3aa552b87baf 100644
--- a/apps/judicial-system/backend/src/app/modules/file/file.service.ts
+++ b/apps/judicial-system/backend/src/app/modules/file/file.service.ts
@@ -405,6 +405,7 @@ export class FileService {
theCase: Case,
file: CaseFile,
timeToLive?: number,
+ useFreshSession = false,
): Promise {
if (this.shouldGetConfirmedDocument(file, theCase)) {
return this.awsS3Service.getConfirmedIndictmentCaseSignedUrl(
@@ -414,10 +415,16 @@ export class FileService {
(content: Buffer) =>
this.confirmIndictmentCaseFile(theCase, file, content),
timeToLive,
+ useFreshSession,
)
}
- return this.awsS3Service.getSignedUrl(theCase.type, file.key, timeToLive)
+ return this.awsS3Service.getSignedUrl(
+ theCase.type,
+ file.key,
+ timeToLive,
+ useFreshSession,
+ )
}
async getCaseFileSignedUrl(
@@ -559,6 +566,7 @@ export class FileService {
theCase,
file,
this.config.robotS3TimeToLiveGet,
+ true,
)
return this.courtService
diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts b/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts
index 0e46c408ac7e..64a57a3ef17c 100644
--- a/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts
+++ b/apps/judicial-system/backend/src/app/modules/file/guards/caseFileCategory.ts
@@ -6,6 +6,7 @@ import {
isDefenceUser,
isIndictmentCase,
isPrisonAdminUser,
+ isPrisonStaffUser,
isRequestCase,
User,
} from '@island.is/judicial-system/types'
@@ -39,6 +40,8 @@ const prisonAdminCaseFileCategories = [
CaseFileCategory.RULING,
]
+const prisonStaffCaseFileCategories = [CaseFileCategory.APPEAL_RULING]
+
export const canLimitedAcccessUserViewCaseFile = (
user: User,
caseType: CaseType,
@@ -68,12 +71,20 @@ export const canLimitedAcccessUserViewCaseFile = (
}
}
- if (
- isPrisonAdminUser(user) &&
- isCompletedCase(caseState) &&
- prisonAdminCaseFileCategories.includes(caseFileCategory)
- ) {
- return true
+ if (isCompletedCase(caseState)) {
+ if (
+ isPrisonStaffUser(user) &&
+ prisonStaffCaseFileCategories.includes(caseFileCategory)
+ ) {
+ return true
+ }
+
+ if (
+ isPrisonAdminUser(user) &&
+ prisonAdminCaseFileCategories.includes(caseFileCategory)
+ ) {
+ return true
+ }
}
return false
diff --git a/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts b/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts
index 13d0b175f3e6..d980ddc5024e 100644
--- a/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/file/guards/test/limitedAccessViewCaseFileGuard.spec.ts
@@ -213,11 +213,7 @@ describe('Limited Access View Case File Guard', () => {
})
})
- describe('prison system users', () => {
- const prisonUser = {
- role: UserRole.PRISON_SYSTEM_STAFF,
- institution: { type: InstitutionType.PRISON },
- }
+ describe('prison admin users', () => {
const prisonAdminUser = {
role: UserRole.PRISON_SYSTEM_STAFF,
institution: { type: InstitutionType.PRISON_ADMIN },
@@ -231,7 +227,7 @@ describe('Limited Access View Case File Guard', () => {
]
describe.each(allowedCaseFileCategories)(
- 'prison system users can view %s',
+ 'prison admin users can view %s',
(category) => {
let thenPrisonAdmin: Then
@@ -256,31 +252,20 @@ describe('Limited Access View Case File Guard', () => {
(category) =>
!allowedCaseFileCategories.includes(category as CaseFileCategory),
),
- )('prison system users can not view %s', (category) => {
- let thenPrison: Then
+ )('prison admin users can not view %s', (category) => {
let thenPrisonAdmin: Then
beforeEach(() => {
- mockRequest.mockImplementationOnce(() => ({
- user: prisonUser,
- case: { type, state },
- caseFile: { category },
- }))
mockRequest.mockImplementationOnce(() => ({
user: prisonAdminUser,
case: { type, state },
caseFile: { category },
}))
- thenPrison = givenWhenThen()
thenPrisonAdmin = givenWhenThen()
})
it('should throw ForbiddenException', () => {
- expect(thenPrison.error).toBeInstanceOf(ForbiddenException)
- expect(thenPrison.error.message).toBe(
- `Forbidden for ${UserRole.PRISON_SYSTEM_STAFF}`,
- )
expect(thenPrisonAdmin.error).toBeInstanceOf(ForbiddenException)
expect(thenPrisonAdmin.error.message).toBe(
`Forbidden for ${UserRole.PRISON_SYSTEM_STAFF}`,
@@ -295,25 +280,108 @@ describe('Limited Access View Case File Guard', () => {
),
)('in state %s', (state) => {
describe.each(Object.keys(CaseFileCategory))(
- 'prison system users can not view %s',
+ 'prison admin users can not view %s',
(category) => {
- let thenPrison: Then
let thenPrisonAdmin: Then
+ beforeEach(() => {
+ mockRequest.mockImplementationOnce(() => ({
+ user: prisonAdminUser,
+ case: { type, state },
+ caseFile: { category },
+ }))
+
+ thenPrisonAdmin = givenWhenThen()
+ })
+
+ it('should throw ForbiddenException', () => {
+ expect(thenPrisonAdmin.error).toBeInstanceOf(ForbiddenException)
+ expect(thenPrisonAdmin.error.message).toBe(
+ `Forbidden for ${UserRole.PRISON_SYSTEM_STAFF}`,
+ )
+ })
+ },
+ )
+ })
+ })
+ })
+
+ describe('prison users', () => {
+ const prisonUser = {
+ role: UserRole.PRISON_SYSTEM_STAFF,
+ institution: { type: InstitutionType.PRISON },
+ }
+
+ describe.each(Object.keys(CaseType))('for %s cases', (type) => {
+ describe.each(completedCaseStates)('in state %s', (state) => {
+ const allowedCaseFileCategories = [CaseFileCategory.APPEAL_RULING]
+
+ describe.each(allowedCaseFileCategories)(
+ 'prison users can view %s',
+ (category) => {
+ let thenPrisonUser: Then
+
beforeEach(() => {
mockRequest.mockImplementationOnce(() => ({
user: prisonUser,
case: { type, state },
caseFile: { category },
}))
+
+ thenPrisonUser = givenWhenThen()
+ })
+
+ it('should activate', () => {
+ expect(thenPrisonUser.result).toBe(true)
+ })
+ },
+ )
+
+ describe.each(
+ Object.keys(CaseFileCategory).filter(
+ (category) =>
+ !allowedCaseFileCategories.includes(category as CaseFileCategory),
+ ),
+ )('prison users can not view %s', (category) => {
+ let thenPrison: Then
+
+ beforeEach(() => {
+ mockRequest.mockImplementationOnce(() => ({
+ user: prisonUser,
+ case: { type, state },
+ caseFile: { category },
+ }))
+
+ thenPrison = givenWhenThen()
+ })
+
+ it('should throw ForbiddenException', () => {
+ expect(thenPrison.error).toBeInstanceOf(ForbiddenException)
+ expect(thenPrison.error.message).toBe(
+ `Forbidden for ${UserRole.PRISON_SYSTEM_STAFF}`,
+ )
+ })
+ })
+ })
+
+ describe.each(
+ Object.keys(CaseState).filter(
+ (state) => !completedCaseStates.includes(state as CaseState),
+ ),
+ )('in state %s', (state) => {
+ describe.each(Object.keys(CaseFileCategory))(
+ 'prison users can not view %s',
+ (category) => {
+ let thenPrison: Then
+
+ beforeEach(() => {
mockRequest.mockImplementationOnce(() => ({
- user: prisonAdminUser,
+ user: prisonUser,
case: { type, state },
caseFile: { category },
}))
thenPrison = givenWhenThen()
- thenPrisonAdmin = givenWhenThen()
})
it('should throw ForbiddenException', () => {
@@ -321,10 +389,6 @@ describe('Limited Access View Case File Guard', () => {
expect(thenPrison.error.message).toBe(
`Forbidden for ${UserRole.PRISON_SYSTEM_STAFF}`,
)
- expect(thenPrisonAdmin.error).toBeInstanceOf(ForbiddenException)
- expect(thenPrisonAdmin.error.message).toBe(
- `Forbidden for ${UserRole.PRISON_SYSTEM_STAFF}`,
- )
})
},
)
diff --git a/apps/judicial-system/backend/src/app/modules/file/limitedAccessFile.controller.ts b/apps/judicial-system/backend/src/app/modules/file/limitedAccessFile.controller.ts
index 395a9ef78d84..3c1605da9c12 100644
--- a/apps/judicial-system/backend/src/app/modules/file/limitedAccessFile.controller.ts
+++ b/apps/judicial-system/backend/src/app/modules/file/limitedAccessFile.controller.ts
@@ -36,6 +36,7 @@ import {
CurrentCase,
LimitedAccessCaseExistsGuard,
} from '../case'
+import { MergedCaseExistsGuard } from '../case/guards/mergedCaseExists.guard'
import { CreateFileDto } from './dto/createFile.dto'
import { CreatePresignedPostDto } from './dto/createPresignedPost.dto'
import { CurrentCaseFile } from './guards/caseFile.decorator'
@@ -107,9 +108,14 @@ export class LimitedAccessFileController {
return this.fileService.createCaseFile(theCase, createFile, user)
}
- @UseGuards(CaseReadGuard, CaseFileExistsGuard, LimitedAccessViewCaseFileGuard)
+ @UseGuards(
+ CaseReadGuard,
+ MergedCaseExistsGuard,
+ CaseFileExistsGuard,
+ LimitedAccessViewCaseFileGuard,
+ )
@RolesRules(prisonSystemStaffRule, defenderRule)
- @Get('file/:fileId/url')
+ @Get(['file/:fileId/url', 'mergedCase/:mergedCaseId/file/:fileId/url'])
@ApiOkResponse({
type: SignedUrl,
description: 'Gets a signed url for a case file',
diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrl.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrl.spec.ts
index 7865f80f3787..c61ce8d1cbf9 100644
--- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrl.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrl.spec.ts
@@ -82,6 +82,7 @@ describe('FileController - Get case file signed url', () => {
theCase.type,
key,
undefined,
+ false,
)
expect(then.result).toEqual({ url })
})
diff --git a/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlGuards.spec.ts
index 009ce1aba49e..88c3d0946486 100644
--- a/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlGuards.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/file/test/fileController/getCaseFileSignedUrlGuards.spec.ts
@@ -3,6 +3,7 @@ import { CanActivate } from '@nestjs/common'
import { RolesGuard } from '@island.is/judicial-system/auth'
import { CaseExistsGuard, CaseReadGuard } from '../../../case'
+import { MergedCaseExistsGuard } from '../../../case/guards/mergedCaseExists.guard'
import { FileController } from '../../file.controller'
import { CaseFileExistsGuard } from '../../guards/caseFileExists.guard'
import { ViewCaseFileGuard } from '../../guards/viewCaseFile.guard'
@@ -18,67 +19,13 @@ describe('FileController - Get case file signed url guards', () => {
)
})
- it('should have five guards', () => {
- expect(guards).toHaveLength(5)
- })
-
- describe('RolesGuard', () => {
- let guard: CanActivate
-
- beforeEach(() => {
- guard = new guards[0]()
- })
-
- it('should have RolesGuard as guard 1', () => {
- expect(guard).toBeInstanceOf(RolesGuard)
- })
- })
-
- describe('CaseExistsGuard', () => {
- let guard: CanActivate
-
- beforeEach(() => {
- guard = new guards[1]()
- })
-
- it('should have CaseExistsGuard as guard 2', () => {
- expect(guard).toBeInstanceOf(CaseExistsGuard)
- })
- })
-
- describe('CaseReadGuard', () => {
- let guard: CanActivate
-
- beforeEach(() => {
- guard = new guards[2]()
- })
-
- it('should have CaseReadGuard as guard 3', () => {
- expect(guard).toBeInstanceOf(CaseReadGuard)
- })
- })
-
- describe('CaseFileExistsGuard', () => {
- let guard: CanActivate
-
- beforeEach(() => {
- guard = new guards[3]()
- })
-
- it('should have CaseFileExistsGuard as guard 4', () => {
- expect(guard).toBeInstanceOf(CaseFileExistsGuard)
- })
- })
-
- describe('ViewCaseFileGuard', () => {
- let guard: CanActivate
-
- beforeEach(() => {
- guard = new guards[4]()
- })
-
- it('should have ViewCaseFileGuard as guard 5', () => {
- expect(guard).toBeInstanceOf(ViewCaseFileGuard)
- })
+ it('should have the right guard configuration', () => {
+ expect(guards).toHaveLength(6)
+ expect(new guards[0]()).toBeInstanceOf(RolesGuard)
+ expect(new guards[1]()).toBeInstanceOf(CaseExistsGuard)
+ expect(new guards[2]()).toBeInstanceOf(CaseReadGuard)
+ expect(new guards[3]()).toBeInstanceOf(MergedCaseExistsGuard)
+ expect(new guards[4]()).toBeInstanceOf(CaseFileExistsGuard)
+ expect(new guards[5]()).toBeInstanceOf(ViewCaseFileGuard)
})
})
diff --git a/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourtOfAppeals.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourtOfAppeals.spec.ts
index d6d768626368..bf3546195e32 100644
--- a/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourtOfAppeals.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/file/test/internalFileController/deliverCaseFileToCourtOfAppeals.spec.ts
@@ -106,6 +106,7 @@ describe('InternalFileController - Deliver case file to court of appeals', () =>
theCase.type,
key,
mockFileConfig.robotS3TimeToLiveGet,
+ true,
)
expect(mockCourtService.updateAppealCaseWithFile).toHaveBeenCalledWith(
user,
diff --git a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrl.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrl.spec.ts
index 0f847a92485b..a25ae133189e 100644
--- a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrl.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrl.spec.ts
@@ -96,6 +96,7 @@ describe('LimitedAccessFileController - Get case file signed url', () => {
theCase.type,
key,
undefined,
+ false,
)
})
})
diff --git a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrlGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrlGuards.spec.ts
index bcd54dc4ef49..93fad8b53589 100644
--- a/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrlGuards.spec.ts
+++ b/apps/judicial-system/backend/src/app/modules/file/test/limitedAccessFileController/getCaseFileSignedUrlGuards.spec.ts
@@ -1,4 +1,5 @@
import { CaseReadGuard } from '../../../case'
+import { MergedCaseExistsGuard } from '../../../case/guards/mergedCaseExists.guard'
import { CaseFileExistsGuard } from '../../guards/caseFileExists.guard'
import { LimitedAccessViewCaseFileGuard } from '../../guards/limitedAccessViewCaseFile.guard'
import { LimitedAccessFileController } from '../../limitedAccessFile.controller'
@@ -15,9 +16,10 @@ describe('LimitedAccessFileController - Get case file signed url guards', () =>
})
it('should have the right guard configuration', () => {
- expect(guards).toHaveLength(3)
+ expect(guards).toHaveLength(4)
expect(new guards[0]()).toBeInstanceOf(CaseReadGuard)
- expect(new guards[1]()).toBeInstanceOf(CaseFileExistsGuard)
- expect(new guards[2]()).toBeInstanceOf(LimitedAccessViewCaseFileGuard)
+ expect(new guards[1]()).toBeInstanceOf(MergedCaseExistsGuard)
+ expect(new guards[2]()).toBeInstanceOf(CaseFileExistsGuard)
+ expect(new guards[3]()).toBeInstanceOf(LimitedAccessViewCaseFileGuard)
})
})
diff --git a/apps/judicial-system/backend/src/app/modules/police/police.service.ts b/apps/judicial-system/backend/src/app/modules/police/police.service.ts
index ac21cd766a84..c4659a31c6de 100644
--- a/apps/judicial-system/backend/src/app/modules/police/police.service.ts
+++ b/apps/judicial-system/backend/src/app/modules/police/police.service.ts
@@ -21,6 +21,7 @@ import {
XRoadMemberClass,
} from '@island.is/shared/utils/server'
+import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters'
import type { User } from '@island.is/judicial-system/types'
import { CaseState, CaseType } from '@island.is/judicial-system/types'
@@ -520,6 +521,9 @@ export class PoliceService {
const { nationalId: defendantNationalId } = defendant
const { name: actor } = user
+ const normalizedNationalId =
+ normalizeAndFormatNationalId(defendantNationalId)[0]
+
const documentName = `Fyrirkall í máli ${workingCase.courtCaseNumber}`
const arraignmentInfo = dateLogs?.find(
(dateLog) => dateLog.dateType === 'ARRAIGNMENT_DATE',
@@ -541,7 +545,7 @@ export class PoliceService {
documentBase64: subpoena,
courtRegistrationDate: arraignmentInfo?.date,
prosecutorSsn: prosecutor?.nationalId,
- prosecutedSsn: defendantNationalId,
+ prosecutedSsn: normalizedNationalId,
courtAddress: court?.address,
courtRoomNumber: arraignmentInfo?.location || '',
courtCeremony: 'Þingfesting',
diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/case.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/case.response.ts
index 4f8159657381..7889f054a2e9 100644
--- a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/case.response.ts
+++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/case.response.ts
@@ -12,7 +12,7 @@ class IndictmentCaseData {
caseNumber!: string
@ApiProperty({ type: Boolean })
- acknowledged?: boolean
+ hasBeenServed?: boolean
@ApiProperty({ type: [Groups] })
groups!: Groups[]
@@ -26,21 +26,22 @@ export class CaseResponse {
data!: IndictmentCaseData
static fromInternalCaseResponse(
- res: InternalCaseResponse,
+ internalCase: InternalCaseResponse,
lang?: string,
): CaseResponse {
const t = getTranslations(lang)
- const defendant = res.defendants[0]
- const subpoenaDateLog = res.dateLogs?.find(
+ const defendant = internalCase.defendants[0] ?? {}
+ const subpoenaDateLog = internalCase.dateLogs?.find(
(dateLog) => dateLog.dateType === DateType.ARRAIGNMENT_DATE,
)
- const subpoenaCreatedDate = subpoenaDateLog?.created.toString() ?? ''
+ const subpoenaCreatedDate = subpoenaDateLog?.created?.toString() ?? '' //TODO: Change to created from subpoena db entry?
+ const subpoenas = defendant.subpoenas ?? []
return {
- caseId: res.id,
+ caseId: internalCase.id,
data: {
- caseNumber: `${t.caseNumber} ${res.courtCaseNumber}`,
- acknowledged: false, // TODO: Connect to real data
+ caseNumber: `${t.caseNumber} ${internalCase.courtCaseNumber}`,
+ hasBeenServed: subpoenas.length > 0 ? subpoenas[0].acknowledged : false,
groups: [
{
label: t.defendant,
@@ -75,23 +76,23 @@ export class CaseResponse {
},
{
label: t.courtCaseNumber,
- value: res.courtCaseNumber,
+ value: internalCase.courtCaseNumber,
},
{
label: t.court,
- value: res.court.name,
+ value: internalCase.court.name,
},
{
label: t.judge,
- value: res.judge.name,
+ value: internalCase.judge.name,
},
{
label: t.institution,
- value: res.prosecutorsOffice.name,
+ value: internalCase.prosecutorsOffice.name,
},
{
label: t.prosecutor,
- value: res.prosecutor.name,
+ value: internalCase.prosecutor.name,
},
],
},
diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/cases.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/cases.response.ts
index b8465d6e2782..bb92cc08c7fa 100644
--- a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/cases.response.ts
+++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/cases.response.ts
@@ -1,3 +1,5 @@
+import { IsEnum } from 'class-validator'
+
import { ApiProperty } from '@nestjs/swagger'
import { isCompletedCase } from '@island.is/judicial-system/types'
@@ -5,6 +7,29 @@ import { isCompletedCase } from '@island.is/judicial-system/types'
import { InternalCasesResponse } from './internal/internalCases.response'
import { getTranslations } from './utils/translations.strings'
+enum TagVariant {
+ BLUE = 'blue',
+ DARKER_BLUE = 'darkerBlue',
+ PURPLE = 'purple',
+ WHITE = 'white',
+ RED = 'red',
+ ROSE = 'rose',
+ BLUEBERRY = 'blueberry',
+ DARK = 'dark',
+ MINT = 'mint',
+ YELLOW = 'yellow',
+ DISABLED = 'disabled',
+ WARN = 'warn',
+}
+
+class StateTag {
+ @IsEnum(TagVariant)
+ @ApiProperty({ enum: TagVariant })
+ color!: TagVariant
+
+ @ApiProperty({ type: String })
+ label!: string
+}
export class CasesResponse {
@ApiProperty({ type: String })
id!: string
@@ -15,11 +40,8 @@ export class CasesResponse {
@ApiProperty({ type: String })
type!: string
- @ApiProperty({ type: Object })
- state!: {
- color: string
- label: string
- }
+ @ApiProperty({ type: StateTag })
+ state!: StateTag
static fromInternalCasesResponse(
response: InternalCasesResponse[],
@@ -31,7 +53,9 @@ export class CasesResponse {
return {
id: item.id,
state: {
- color: isCompletedCase(item.state) ? 'purple' : 'blue',
+ color: isCompletedCase(item.state)
+ ? TagVariant.PURPLE
+ : TagVariant.BLUE,
label: isCompletedCase(item.state) ? t.completed : t.active,
},
caseNumber: `${t.caseNumber} ${item.courtCaseNumber}`,
diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalCase.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalCase.response.ts
index 0f310e71aa91..72bd4ccfa847 100644
--- a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalCase.response.ts
+++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/internal/internalCase.response.ts
@@ -28,6 +28,7 @@ interface Defendant {
defenderEmail?: string
defenderPhoneNumber?: string
defenderChoice?: DefenderChoice
+ subpoenas?: Subpoena[]
}
interface DateLog {
@@ -37,3 +38,9 @@ interface DateLog {
date: Date
location?: string
}
+
+interface Subpoena {
+ id: string
+ subpoenaId: string
+ acknowledged: boolean
+}
diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts
index ffbacab59c49..342e0ed294f5 100644
--- a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts
+++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts
@@ -12,6 +12,14 @@ import { InternalCaseResponse } from './internal/internalCase.response'
import { Groups } from './shared/groups.model'
import { getTranslations } from './utils/translations.strings'
+enum AlertMessageType {
+ ERROR = 'error',
+ INFO = 'info',
+ SUCCESS = 'success',
+ WARNING = 'warning',
+ DEFAULT = 'default',
+}
+
class DefenderInfo {
@IsEnum(DefenderChoice)
@ApiProperty({ enum: DefenderChoice })
@@ -19,17 +27,38 @@ class DefenderInfo {
@ApiProperty({ type: () => String })
defenderName?: string
+
+ @ApiProperty({ type: () => Boolean })
+ canEdit?: boolean
+
+ @ApiProperty({ type: () => String })
+ courtContactInfo?: string
+}
+
+class AlertMessage {
+ @IsEnum(AlertMessageType)
+ @ApiProperty({ enum: AlertMessageType })
+ type?: AlertMessageType
+
+ @ApiProperty({ type: () => String })
+ message?: string
}
class SubpoenaData {
@ApiProperty({ type: () => String })
title!: string
- @ApiProperty({ type: Boolean })
- acknowledged?: boolean
+ @ApiProperty({ type: String })
+ subtitle?: string
@ApiProperty({ type: () => [Groups] })
groups!: Groups[]
+
+ @ApiProperty({ type: () => [AlertMessage] })
+ alerts?: AlertMessage[]
+
+ @ApiProperty({ type: Boolean })
+ hasBeenServed?: boolean
}
export class SubpoenaResponse {
@@ -59,31 +88,52 @@ export class SubpoenaResponse {
const waivedRight = defendantInfo?.defenderChoice === DefenderChoice.WAIVE
const hasDefender = defendantInfo?.defenderName !== undefined
+ const subpoena = defendantInfo?.subpoenas ?? []
+ const hasBeenServed = subpoena[0]?.acknowledged ?? false
+ const canChangeDefenseChoice = !waivedRight && !hasDefender
const subpoenaDateLog = internalCase.dateLogs?.find(
(dateLog) => dateLog.dateType === DateType.ARRAIGNMENT_DATE,
)
const arraignmentDate = subpoenaDateLog?.date ?? ''
const subpoenaCreatedDate = subpoenaDateLog?.created ?? '' //TODO: Change to subpoena created in RLS
+ const arraignmentLocation = subpoenaDateLog?.location
+ ? `${internalCase.court.name}, Dómsalur ${subpoenaDateLog.location}`
+ : internalCase.court.name
+ const courtNameAndAddress = `${internalCase.court.name}, ${internalCase.court.address}`
return {
caseId: internalCase.id,
data: {
title: t.subpoena,
- acknowledged: false, // TODO: Connect to real data
+ subtitle: courtNameAndAddress,
+ hasBeenServed: hasBeenServed,
+ alerts: [
+ ...(hasBeenServed
+ ? [
+ {
+ type: AlertMessageType.SUCCESS,
+ message: t.subpoenaServed,
+ },
+ ]
+ : []),
+ ],
groups: [
{
label: `${t.caseNumber} ${internalCase.courtCaseNumber}`,
items: [
[t.date, formatDate(subpoenaCreatedDate, 'PP')],
- [t.institution, 'Lögreglustjórinn á höfuðborgarsvæðinu'],
+ [
+ t.institution,
+ internalCase.prosecutor?.institution?.name ?? t.notAvailable,
+ ],
[t.prosecutor, internalCase.prosecutor?.name],
[t.accused, defendantInfo?.name],
[
t.arraignmentDate,
formatDate(arraignmentDate, "d.M.yyyy 'kl.' HH:mm"),
],
- [t.location, subpoenaDateLog?.location ?? ''],
+ [t.location, arraignmentLocation],
[t.courtCeremony, t.parliamentaryConfirmation],
].map((item) => ({
label: item[0] ?? '',
@@ -100,6 +150,10 @@ export class SubpoenaResponse {
!waivedRight && hasDefender
? defendantInfo?.defenderName
: undefined,
+ canEdit: canChangeDefenseChoice,
+ courtContactInfo: canChangeDefenseChoice
+ ? undefined
+ : t.courtContactInfo,
}
: undefined,
}
diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/utils/translations.strings.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/utils/translations.strings.ts
index 112520c77735..6ca747677b74 100644
--- a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/utils/translations.strings.ts
+++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/utils/translations.strings.ts
@@ -11,6 +11,7 @@ type Translations = {
court: string
courtCaseNumber: string
courtCeremony: string
+ courtContactInfo: string
date: string
defendant: string
defender: string
@@ -29,6 +30,7 @@ type Translations = {
prosecutorsOffice: string
subpoena: string
subpoenaSent: string
+ subpoenaServed: string
type: string
waiveRightToCounsel: string
}
@@ -45,6 +47,8 @@ const translations: Translations = {
court: 'Court',
courtCaseNumber: 'Case number',
courtCeremony: 'Court ceremony',
+ courtContactInfo:
+ 'Please contact the court if you wish to change your choice of defender',
date: 'Date',
defendant: 'Defendant',
defender: 'Defender',
@@ -63,6 +67,8 @@ const translations: Translations = {
prosecutorsOffice: 'Institution',
subpoena: 'Subpoena',
subpoenaSent: 'Subpoena sent',
+ subpoenaServed:
+ 'Confirmation of subpoena service has been sent to the court',
type: 'Type',
waiveRightToCounsel: 'Right to counsel waived',
},
@@ -76,6 +82,8 @@ const translations: Translations = {
court: 'Dómstóll',
courtCaseNumber: 'Málsnúmer héraðsdóms',
courtCeremony: 'Dómsathöfn',
+ courtContactInfo:
+ 'Vinsamlegast hafið samband við dómstól til að breyta verjanda vali',
date: 'Dagsetning',
defendant: 'Varnaraðili',
defender: 'Verjandi',
@@ -94,6 +102,7 @@ const translations: Translations = {
prosecutorsOffice: 'Embætti',
subpoena: 'Fyrirkall',
subpoenaSent: 'Fyrirkall sent',
+ subpoenaServed: 'Staðfesting á móttöku hefur verið send á dómstóla',
type: 'Tegund',
waiveRightToCounsel: 'Ekki er óskað eftir verjanda',
},
diff --git a/apps/judicial-system/web/messages/Core/errors.ts b/apps/judicial-system/web/messages/Core/errors.ts
index 406e594e9e3e..3123d4e8745c 100644
--- a/apps/judicial-system/web/messages/Core/errors.ts
+++ b/apps/judicial-system/web/messages/Core/errors.ts
@@ -12,18 +12,36 @@ export const errors = defineMessages({
description:
'Notaður sem villuskilaboð þegar ekki gengur að uppfæra varnaraðila',
},
+ updateCivilClaimant: {
+ id: 'judicial.system.core:errors.update_civil_claimant',
+ defaultMessage: 'Upp kom villa við að uppfæra kröfuhafa',
+ description:
+ 'Notaður sem villuskilaboð þegar ekki gengur að uppfæra kröfuhafa',
+ },
createDefendant: {
id: 'judicial.system.core:errors.create_defendant',
defaultMessage: 'Upp kom villa við að stofna nýjan varnaraðila',
description:
'Notaður sem villuskilaboð þegar ekki gengur að stofna varnaraðila',
},
+ createCivilClaimant: {
+ id: 'judicial.system.core:errors.create_civil_claimant',
+ defaultMessage: 'Upp kom villa við að stofna nýjan kröfuhafa',
+ description:
+ 'Notaður sem villuskilaboð þegar ekki gengur að stofna kröfuhafa',
+ },
deleteDefendant: {
id: 'judicial.system.core:errors.delete_defendant',
defaultMessage: 'Upp kom villa við að eyða varnaraðila',
description:
'Notaður sem villuskilaboð þegar ekki gengur að eyða varnaraðila',
},
+ deleteCivilClaimant: {
+ id: 'judicial.system.core:errors.delete_civil_claimant',
+ defaultMessage: 'Upp kom villa við að eyða kröfuhafa',
+ description:
+ 'Notaður sem villuskilaboð þegar ekki gengur að eyða kröfuhafa',
+ },
createCase: {
id: 'judicial.system.core:errors.create_case',
defaultMessage: 'Upp kom villa við að stofnun máls',
diff --git a/apps/judicial-system/web/src/components/AccordionItems/ConnectedCaseFilesAccordionItem/ConnectedCaseFilesAccordionItem.tsx b/apps/judicial-system/web/src/components/AccordionItems/ConnectedCaseFilesAccordionItem/ConnectedCaseFilesAccordionItem.tsx
index c65bf82aa94c..631bf925ccf3 100644
--- a/apps/judicial-system/web/src/components/AccordionItems/ConnectedCaseFilesAccordionItem/ConnectedCaseFilesAccordionItem.tsx
+++ b/apps/judicial-system/web/src/components/AccordionItems/ConnectedCaseFilesAccordionItem/ConnectedCaseFilesAccordionItem.tsx
@@ -8,10 +8,14 @@ import { Case } from '@island.is/judicial-system-web/src/graphql/schema'
import { strings } from './ConnectedCaseFilesAccordionItem.strings'
interface Props {
+ connectedCaseParentId: string
connectedCase: Case
}
-const ConnectedCaseFilesAccordionItem: FC = ({ connectedCase }) => {
+const ConnectedCaseFilesAccordionItem: FC = ({
+ connectedCaseParentId,
+ connectedCase,
+}) => {
const { formatMessage } = useIntl()
const { caseFiles, courtCaseNumber } = connectedCase
@@ -30,6 +34,7 @@ const ConnectedCaseFilesAccordionItem: FC = ({ connectedCase }) => {
)
diff --git a/apps/judicial-system/web/src/components/AppealCaseFilesOverview/AppealCaseFilesOverview.tsx b/apps/judicial-system/web/src/components/AppealCaseFilesOverview/AppealCaseFilesOverview.tsx
index 843a2bfdb20f..2b49c6b6e017 100644
--- a/apps/judicial-system/web/src/components/AppealCaseFilesOverview/AppealCaseFilesOverview.tsx
+++ b/apps/judicial-system/web/src/components/AppealCaseFilesOverview/AppealCaseFilesOverview.tsx
@@ -127,7 +127,6 @@ const AppealCaseFilesOverview = () => {
onOpen(file.id)}
diff --git a/apps/judicial-system/web/src/components/DefenderInfo/DefenderInfo.tsx b/apps/judicial-system/web/src/components/DefenderInfo/DefenderInfo.tsx
index 4b66b1f1b215..6c007e693019 100644
--- a/apps/judicial-system/web/src/components/DefenderInfo/DefenderInfo.tsx
+++ b/apps/judicial-system/web/src/components/DefenderInfo/DefenderInfo.tsx
@@ -17,8 +17,7 @@ import { TempCase as Case } from '@island.is/judicial-system-web/src/types'
import { useCase } from '../../utils/hooks'
import RequiredStar from '../RequiredStar/RequiredStar'
import { UserContext } from '../UserProvider/UserProvider'
-import { BlueBox, SectionHeading } from '..'
-import DefenderInput from './DefenderInput'
+import { BlueBox, InputAdvocate, SectionHeading } from '..'
import DefenderNotFound from './DefenderNotFound'
import { defenderInfo } from './DefenderInfo.strings'
@@ -94,7 +93,7 @@ const DefenderInfo: FC = ({ workingCase, setWorkingCase }) => {
/>
{defenderNotFound && }
-
+
{isProsecutionUser(user) && (
<>
diff --git a/apps/judicial-system/web/src/components/FormProvider/case.graphql b/apps/judicial-system/web/src/components/FormProvider/case.graphql
index 70cdc86833c7..f6eeefc5ba75 100644
--- a/apps/judicial-system/web/src/components/FormProvider/case.graphql
+++ b/apps/judicial-system/web/src/components/FormProvider/case.graphql
@@ -299,11 +299,16 @@ query Case($input: CaseQueryInput!) {
caseFiles {
id
created
+ modified
name
+ type
+ category
state
key
size
- category
+ userGeneratedFilename
+ displayDate
+ submittedBy
}
policeCaseNumbers
indictmentSubtypes
diff --git a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql
index 594219f55ff9..774715ba84f3 100644
--- a/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql
+++ b/apps/judicial-system/web/src/components/FormProvider/limitedAccessCase.graphql
@@ -10,10 +10,13 @@ query LimitedAccessCase($input: CaseQueryInput!) {
caseFiles {
id
created
+ modified
name
+ type
category
+ state
key
- policeCaseNumber
+ size
userGeneratedFilename
displayDate
submittedBy
@@ -165,5 +168,54 @@ query LimitedAccessCase($input: CaseQueryInput!) {
id
courtCaseNumber
}
+ indictmentCounts {
+ id
+ caseId
+ policeCaseNumber
+ created
+ modified
+ vehicleRegistrationNumber
+ offenses
+ substances
+ lawsBroken
+ incidentDescription
+ legalArguments
+ }
+ mergedCases {
+ id
+ courtCaseNumber
+ type
+ caseFiles {
+ id
+ created
+ modified
+ name
+ type
+ category
+ state
+ key
+ size
+ userGeneratedFilename
+ displayDate
+ submittedBy
+ }
+ policeCaseNumbers
+ indictmentSubtypes
+ }
+ hasCivilClaims
+ civilClaimants {
+ id
+ caseId
+ name
+ nationalId
+ noNationalId
+ hasSpokesperson
+ spokespersonIsLawyer
+ spokespersonNationalId
+ spokespersonName
+ spokespersonEmail
+ spokespersonPhoneNumber
+ caseFilesSharedWithSpokesperson
+ }
}
}
diff --git a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.strings.ts b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.strings.ts
index 7e93be0a3dfc..8372e2fbbc45 100644
--- a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.strings.ts
+++ b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.strings.ts
@@ -28,9 +28,21 @@ export const strings = defineMessages({
description:
'Notaður sem titill á innsend gögn hluta á dómskjalaskjá í ákærum.',
},
+ subpoenaTitle: {
+ id: 'judicial.system.core:court.indictment_case_files_list.subpoena_title',
+ defaultMessage: 'Fyrirkall',
+ description:
+ 'Notaður sem titill á firyrkall hluta á dómskjalaskjá í ákærum.',
+ },
civilClaimsTitle: {
id: 'judicial.system.core:indictment_case_files_list.civil_claims_title',
defaultMessage: 'Einkaréttarkröfur',
description: 'Notaður sem titill á dómskjalaskjá í ákærum.',
},
+ subpoenaButtonText: {
+ id: 'judicial.system.indictments:indictment_case_files_list.subpoena_button_text',
+ defaultMessage: 'Fyrirkall {name}.pdf',
+ description:
+ 'Notaður sem texti á PDF takka til að sækja firyrkall í ákærum.',
+ },
})
diff --git a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx
index f28a239cf977..99d714df2a3e 100644
--- a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx
+++ b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx
@@ -32,6 +32,7 @@ import { strings } from './IndictmentCaseFilesList.strings'
interface Props {
workingCase: Case
displayHeading?: boolean
+ connectedCaseParentId?: string
}
interface RenderFilesProps {
@@ -39,17 +40,15 @@ interface RenderFilesProps {
onOpenFile: (fileId: string) => void
}
-export const RenderFiles: FC = ({
+export const RenderFiles: FC = ({
caseFiles,
onOpenFile,
- workingCase,
}) => {
return (
<>
{caseFiles.map((file) => (
= ({
const IndictmentCaseFilesList: FC = ({
workingCase,
displayHeading = true,
+ connectedCaseParentId,
}) => {
const { formatMessage } = useIntl()
const { user } = useContext(UserContext)
const { onOpen, fileNotFound, dismissFileNotFound } = useFileList({
caseId: workingCase.id,
+ connectedCaseParentId,
})
const showTrafficViolationCaseFiles = isTrafficViolationCase(workingCase)
+ const showSubpoenaPdf = workingCase.arraignmentDate
const cf = workingCase.caseFiles
@@ -115,11 +117,7 @@ const IndictmentCaseFilesList: FC = ({
{formatMessage(caseFiles.indictmentSection)}
-
+
)}
{showTrafficViolationCaseFiles && (
@@ -130,6 +128,7 @@ const IndictmentCaseFilesList: FC = ({
= ({
{formatMessage(caseFiles.criminalRecordSection)}
-
+
)}
{criminalRecordUpdate &&
@@ -158,11 +153,7 @@ const IndictmentCaseFilesList: FC = ({
{formatMessage(caseFiles.criminalRecordUpdateSection)}
-
+
)}
{costBreakdowns && costBreakdowns.length > 0 && (
@@ -170,11 +161,7 @@ const IndictmentCaseFilesList: FC = ({
{formatMessage(caseFiles.costBreakdownSection)}
-
+
)}
{others && others.length > 0 && (
@@ -182,11 +169,7 @@ const IndictmentCaseFilesList: FC = ({
{formatMessage(caseFiles.otherDocumentsSection)}
-
+
)}
@@ -197,6 +180,7 @@ const IndictmentCaseFilesList: FC = ({
= ({
{formatMessage(strings.rulingAndCourtRecordsTitle)}
{courtRecords && courtRecords.length > 0 && (
-
+
)}
{(isDistrictCourtUser(user) || isCompletedCase(workingCase.state)) &&
rulings &&
rulings.length > 0 && (
-
+
)}
) : null}
@@ -240,11 +216,7 @@ const IndictmentCaseFilesList: FC = ({
{formatMessage(strings.civilClaimsTitle)}
-
+
)}
{uploadedCaseFiles && uploadedCaseFiles.length > 0 && (
@@ -255,6 +227,28 @@ const IndictmentCaseFilesList: FC = ({
)}
+ {showSubpoenaPdf &&
+ workingCase.defendants &&
+ workingCase.defendants.length > 0 && (
+
+
+ {formatMessage(strings.subpoenaTitle)}
+
+ {workingCase.defendants.map((defendant) => (
+
+
+
+ ))}
+
+ )}
{fileNotFound && }
diff --git a/apps/judicial-system/web/src/components/InfoCard/CivilClaimantInfo/CivilClaimantInfo.strings.ts b/apps/judicial-system/web/src/components/InfoCard/CivilClaimantInfo/CivilClaimantInfo.strings.ts
new file mode 100644
index 000000000000..2c7f6e80f607
--- /dev/null
+++ b/apps/judicial-system/web/src/components/InfoCard/CivilClaimantInfo/CivilClaimantInfo.strings.ts
@@ -0,0 +1,20 @@
+import { defineMessages } from 'react-intl'
+
+export const strings = defineMessages({
+ lawyer: {
+ id: 'judicial.system.core:info_card.civil_claimant_info.lawyer',
+ defaultMessage: 'Lögmaður',
+ description: 'Notaður sem titill á lögmanni kröfuhafa.',
+ },
+ noLawyer: {
+ id: 'judicial.system.core:info_card.civil_claimant_info.no_lawyer',
+ defaultMessage: 'Hefur ekki verið skráður',
+ description: 'Notaður sem texti þegar lögmaður kröfuhafa er ekki skráður.',
+ },
+ spokesperson: {
+ id: 'judicial.system.core:info_card.civil_claimant_info.spokesperson',
+ defaultMessage: 'Réttargæslumaður',
+ description:
+ 'Notaður sem titill á lögmanni kröfuhafa ef hann er réttargæslumanður.',
+ },
+})
diff --git a/apps/judicial-system/web/src/components/InfoCard/CivilClaimantInfo/CivilClaimantInfo.tsx b/apps/judicial-system/web/src/components/InfoCard/CivilClaimantInfo/CivilClaimantInfo.tsx
new file mode 100644
index 000000000000..367edde71515
--- /dev/null
+++ b/apps/judicial-system/web/src/components/InfoCard/CivilClaimantInfo/CivilClaimantInfo.tsx
@@ -0,0 +1,50 @@
+import { FC } from 'react'
+import { useIntl } from 'react-intl'
+
+import { Box, Text } from '@island.is/island-ui/core'
+import { formatDOB } from '@island.is/judicial-system/formatters'
+import { CivilClaimant } from '@island.is/judicial-system-web/src/graphql/schema'
+
+import RenderPersonalData from '../RenderPersonalInfo/RenderPersonalInfo'
+import { strings } from './CivilClaimantInfo.strings'
+
+interface CivilClaimantInfoProps {
+ civilClaimant: CivilClaimant
+}
+
+export const CivilClaimantInfo: FC = (props) => {
+ const { civilClaimant } = props
+ const { formatMessage } = useIntl()
+
+ return (
+
+
+ {civilClaimant.name}
+ {civilClaimant.nationalId &&
+ `, ${formatDOB(
+ civilClaimant.nationalId,
+ civilClaimant.noNationalId,
+ )}`}
+
+ {civilClaimant.hasSpokesperson ? (
+
+
+ {civilClaimant.spokespersonIsLawyer
+ ? `${formatMessage(strings.lawyer)}: `
+ : `${formatMessage(strings.spokesperson)}: `}
+
+ {RenderPersonalData(
+ civilClaimant.spokespersonName,
+ civilClaimant.spokespersonEmail,
+ civilClaimant.spokespersonPhoneNumber,
+ false,
+ )}
+
+ ) : (
+ {`${formatMessage(strings.lawyer)}: ${formatMessage(
+ strings.noLawyer,
+ )}`}
+ )}
+
+ )
+}
diff --git a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts
index 3bd4c5af473a..1944e2e813ed 100644
--- a/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts
+++ b/apps/judicial-system/web/src/components/InfoCard/DefendantInfo/DefendantInfo.strings.ts
@@ -33,12 +33,6 @@ export const strings = defineMessages({
defaultMessage: 'Dómur birtur {date}',
description: 'Notað til að birta dagsetningu þegar dómur var birtur.',
},
- noDefenderAssigned: {
- id: 'judicial.system.core:info_card.defendant_info.no_defender_assigned',
- defaultMessage: 'Ekki skráður',
- description:
- 'Notað til að láta vita að enginn verjandi er skráður í ákæru.',
- },
spokesperson: {
id: 'judicial.system.core:info_card.spokesperson',
defaultMessage: 'Talsmaður',
diff --git a/apps/judicial-system/web/src/components/InfoCard/InfoCardActiveIndictment.tsx b/apps/judicial-system/web/src/components/InfoCard/InfoCardActiveIndictment.tsx
index f24fec7cc266..e4833495f1d9 100644
--- a/apps/judicial-system/web/src/components/InfoCard/InfoCardActiveIndictment.tsx
+++ b/apps/judicial-system/web/src/components/InfoCard/InfoCardActiveIndictment.tsx
@@ -18,18 +18,18 @@ const InfoCardActiveIndictment = () => {
mergedCaseProsecutor,
mergedCaseJudge,
mergedCaseCourt,
+ civilClaimants,
} = useInfoCardItems()
return (
= (props) => {
indictmentReviewer,
indictmentReviewDecision,
indictmentReviewedDate,
+ civilClaimants,
} = useInfoCardItems()
const {
@@ -54,6 +55,9 @@ const InfoCardClosedIndictment: FC = (props) => {
),
],
},
+ ...(workingCase.hasCivilClaims
+ ? [{ id: 'civil-claimant-section', items: [civilClaimants] }]
+ : []),
{
id: 'case-info-section',
items: [
diff --git a/apps/judicial-system/web/src/components/InfoCard/InfoCardIndictment.strings.ts b/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.strings.ts
similarity index 82%
rename from apps/judicial-system/web/src/components/InfoCard/InfoCardIndictment.strings.ts
rename to apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.strings.ts
index f936e77d0473..27405ff2a85c 100644
--- a/apps/judicial-system/web/src/components/InfoCard/InfoCardIndictment.strings.ts
+++ b/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.strings.ts
@@ -27,16 +27,6 @@ export const strings = defineMessages({
defaultMessage: 'Ákvörðun',
description: 'Notaður sem titill á "ákvörðun" hluta af yfirliti ákæru.',
},
- indictmentDefendant: {
- id: 'judicial.system.core:info_card_indictment.indictment_defendant',
- defaultMessage: 'Dómfelldi',
- description: 'Notaður sem titill á "dómfelldi" hluta af yfirliti ákæru.',
- },
- indictmentDefendants: {
- id: 'judicial.system.core:info_card_indictment.indictment_defendants',
- defaultMessage: 'Dómfelldu',
- description: 'Notaður sem titill á "dómfelldu" hluta af yfirliti ákæru.',
- },
reviewTagAppealed: {
id: 'judicial.system.core:info_card_indictment.review_tag_appealed_v1',
defaultMessage: 'Áfrýja dómi',
@@ -63,4 +53,16 @@ export const strings = defineMessages({
defaultMessage: 'Sameinað úr',
description: 'Notaður sem titill á "Sameinað úr" hluta af yfirliti ákæru.',
},
+ civilClaimant: {
+ id: 'judicial.system.core:info_card_indictment.civil_claimant',
+ defaultMessage: 'Kröfuhafi',
+ description:
+ 'Notaður sem titill á "kröfuhafa" hluta í yfirliti ákæru þegar kröfuhafi er einn.',
+ },
+ civilClaimants: {
+ id: 'judicial.system.core:info_card_indictment.civil_claimants',
+ defaultMessage: 'Kröfuhafar',
+ description:
+ 'Notaður sem titill á "kröfuhafar" hluta í yfirliti ákæru þegar kröfuhafar eru fleiri en einn.',
+ },
})
diff --git a/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.tsx b/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.tsx
index a03a766414c7..1cc5f68fb86c 100644
--- a/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.tsx
+++ b/apps/judicial-system/web/src/components/InfoCard/useInfoCardItems.tsx
@@ -20,13 +20,14 @@ import {
import { sortByIcelandicAlphabet } from '../../utils/sortHelper'
import { FormContext } from '../FormProvider/FormProvider'
+import { CivilClaimantInfo } from './CivilClaimantInfo/CivilClaimantInfo'
import {
DefendantInfo,
DefendantInfoActionButton,
} from './DefendantInfo/DefendantInfo'
import RenderPersonalData from './RenderPersonalInfo/RenderPersonalInfo'
import { Item } from './InfoCard'
-import { strings } from './InfoCardIndictment.strings'
+import { strings } from './useInfoCardItems.strings'
const useInfoCardItems = () => {
const { formatMessage } = useIntl()
@@ -297,6 +298,23 @@ const useInfoCardItems = () => {
],
}
+ const civilClaimants: Item = {
+ id: 'civil-claimant-item',
+ title: capitalize(
+ workingCase.civilClaimants && workingCase.civilClaimants.length > 1
+ ? formatMessage(strings.civilClaimants)
+ : formatMessage(strings.civilClaimant),
+ ),
+ values: workingCase.civilClaimants
+ ? workingCase.civilClaimants.map((civilClaimant) => (
+
+ ))
+ : [],
+ }
+
return {
defendants,
indictmentCreated,
@@ -324,6 +342,7 @@ const useInfoCardItems = () => {
indictmentReviewDecision,
indictmentReviewedDate,
parentCaseValidToDate,
+ civilClaimants,
}
}
diff --git a/apps/judicial-system/web/src/components/DefenderInfo/DefenderInput.strings.ts b/apps/judicial-system/web/src/components/Inputs/Input.strings.ts
similarity index 68%
rename from apps/judicial-system/web/src/components/DefenderInfo/DefenderInput.strings.ts
rename to apps/judicial-system/web/src/components/Inputs/Input.strings.ts
index 06367562df1b..80f1a51471c2 100644
--- a/apps/judicial-system/web/src/components/DefenderInfo/DefenderInput.strings.ts
+++ b/apps/judicial-system/web/src/components/Inputs/Input.strings.ts
@@ -1,12 +1,17 @@
import { defineMessages } from 'react-intl'
-export const defenderInput = defineMessages({
+export const strings = defineMessages({
nameLabel: {
id: 'judicial.system.core:defender_input.name_label',
defaultMessage:
'Nafn {sessionArrangements, select, ALL_PRESENT_SPOKESPERSON {talsmanns} other {verjanda}}',
description: 'Notaður sem titill á inputi fyrir skipaðan verjanda.',
},
+ spokespersonNameLabel: {
+ id: 'judicial.system.core:defender_input.spokesperson_name_label',
+ defaultMessage: 'Nafn réttargæslumanns',
+ description: 'Notaður sem titill á inputi fyrir skipaðan verjanda.',
+ },
namePlaceholder: {
id: 'judicial.system.core:defender_input.name_placeholder',
defaultMessage: 'Fult nafn',
@@ -19,6 +24,11 @@ export const defenderInput = defineMessages({
description:
'Notaður sem titill á inputi fyrir netfang skipaðans verjanda.',
},
+ spokespersonEmailLabel: {
+ id: 'judicial.system.core:defender_input.spokesperson_email_label',
+ defaultMessage: 'Netfang réttargæslumanns',
+ description: 'Notaður sem titill á inputi fyrir skipaðan verjanda.',
+ },
emailPlaceholder: {
id: 'judicial.system.core:defender_input.email_placeholder',
defaultMessage: 'Netfang',
@@ -32,6 +42,11 @@ export const defenderInput = defineMessages({
description:
'Notaður sem titill á inputi fyrir símanúmer skipaðans verjanda.',
},
+ spokespersonPhoneNumberLabel: {
+ id: 'judicial.system.core:defender_input.spokesperson_phone_number_label',
+ defaultMessage: 'Símanúmer réttargæslumanns',
+ description: 'Notaður sem titill á inputi fyrir skipaðan verjanda.',
+ },
phoneNumberPlaceholder: {
id: 'judicial.system.core:defender_input.phone_number_placeholder',
defaultMessage: 'Símanúmer',
diff --git a/apps/judicial-system/web/src/components/DefenderInfo/DefenderInput.tsx b/apps/judicial-system/web/src/components/Inputs/InputAdvocate.tsx
similarity index 55%
rename from apps/judicial-system/web/src/components/DefenderInfo/DefenderInput.tsx
rename to apps/judicial-system/web/src/components/Inputs/InputAdvocate.tsx
index bb0b879fc0bc..e5bfaec3c2fe 100644
--- a/apps/judicial-system/web/src/components/DefenderInfo/DefenderInput.tsx
+++ b/apps/judicial-system/web/src/components/Inputs/InputAdvocate.tsx
@@ -27,17 +27,20 @@ import {
} from '@island.is/judicial-system-web/src/utils/formHelper'
import {
useCase,
+ useCivilClaimants,
useDefendants,
useGetLawyers,
} from '@island.is/judicial-system-web/src/utils/hooks'
import { Validation } from '@island.is/judicial-system-web/src/utils/validate'
-import { defenderInput as m } from './DefenderInput.strings'
+import { strings } from './Input.strings'
interface Props {
- onDefenderNotFound: (defenderNotFound: boolean) => void
+ onAdvocateNotFound?: (advocateNotFound: boolean) => void
disabled?: boolean | null
- defendantId?: string | null
+ clientId?: string | null
+ advocateType?: 'defender' | 'spokesperson' | 'legal_rights_protector'
+ isCivilClaim?: boolean
}
interface PropertyValidation {
@@ -48,12 +51,38 @@ interface PropertyValidation {
}
}
-type InputType = 'defenderEmail' | 'defenderPhoneNumber'
+type InputType =
+ | 'defenderEmail'
+ | 'defenderPhoneNumber'
+ | 'spokespersonEmail'
+ | 'spokespersonPhoneNumber'
+
+/**
+ * A component that handles setting any kind of legal advocate. In doing so
+ * there are three things to consider.
+ *
+ * 1. In R-cases, a single *defender* is set on the case itself.
+ * 2. In S-cases, a *defender* or *spokesperson* is set on each defendant,
+ * depending on what SESSION_ARRANGEMENT is set.
+ * 3. In S-cases, a *legal rights protector* is set on each civil claimant.
+ */
+const InputAdvocate: FC = ({
+ // A function that runs if an advocate is not found.
+ onAdvocateNotFound,
+
+ /**
+ * The id of the client of the advocate. Used to update the advocate info
+ * of the client.
+ */
+ clientId,
+
+ // The type of advocate being set. See description above.
+ advocateType,
+
+ // If set to true, the defender info is set on a civil claimant in a case.
+ isCivilClaim = false,
-const DefenderInput: FC = ({
- onDefenderNotFound,
disabled,
- defendantId,
}) => {
const { workingCase, setWorkingCase } = useContext(FormContext)
const { formatMessage } = useIntl()
@@ -61,12 +90,21 @@ const DefenderInput: FC = ({
const { updateCase, setAndSendCaseToServer } = useCase()
const { updateDefendant, updateDefendantState, setAndSendDefendantToServer } =
useDefendants()
+ const {
+ setAndSendCivilClaimantToServer,
+ updateCivilClaimantState,
+ updateCivilClaimant,
+ } = useCivilClaimants()
const [emailErrorMessage, setEmailErrorMessage] = useState('')
const [phoneNumberErrorMessage, setPhoneNumberErrorMessage] =
useState('')
const defendantInDefendants = workingCase.defendants?.find(
- (defendant) => defendant.id === defendantId,
+ (defendant) => defendant.id === clientId,
+ )
+
+ const civilClaimantInCivilClaimants = workingCase.civilClaimants?.find(
+ (civilClaimant) => civilClaimant.id === clientId,
)
const options = useMemo(
@@ -80,7 +118,11 @@ const DefenderInput: FC = ({
)
const handleLawyerChange = useCallback(
- (selectedOption: SingleValue) => {
+ (
+ selectedOption: SingleValue,
+ isCivilClaim: boolean,
+ clientId?: string | null,
+ ) => {
let updatedLawyer = {
defenderName: '',
defenderNationalId: '',
@@ -88,26 +130,52 @@ const DefenderInput: FC = ({
defenderPhoneNumber: '',
}
+ let updatedSpokesperson = {
+ spokespersonName: '',
+ spokespersonNationalId: '',
+ spokespersonEmail: '',
+ spokespersonPhoneNumber: '',
+ }
+
if (selectedOption) {
const { label, value, __isNew__: defenderNotFound } = selectedOption
- onDefenderNotFound(defenderNotFound || false)
+ onAdvocateNotFound && onAdvocateNotFound(defenderNotFound || false)
const lawyer = lawyers.find(
(l: Lawyer) => l.email === (value as string),
)
-
updatedLawyer = {
defenderName: lawyer ? lawyer.name : label,
defenderNationalId: lawyer ? lawyer.nationalId : '',
defenderEmail: lawyer ? lawyer.email : '',
defenderPhoneNumber: lawyer ? lawyer.phoneNr : '',
}
+
+ updatedSpokesperson = {
+ spokespersonName: lawyer ? lawyer.name : label,
+ spokespersonNationalId: lawyer ? lawyer.nationalId : '',
+ spokespersonEmail: lawyer ? lawyer.email : '',
+ spokespersonPhoneNumber: lawyer ? lawyer.phoneNr : '',
+ }
}
- if (defendantId) {
+ if (isCivilClaim && clientId) {
+ setAndSendCivilClaimantToServer(
+ {
+ ...updatedSpokesperson,
+ caseId: workingCase.id,
+ civilClaimantId: clientId,
+ caseFilesSharedWithSpokesperson:
+ updatedSpokesperson.spokespersonNationalId
+ ? civilClaimantInCivilClaimants?.caseFilesSharedWithSpokesperson
+ : null,
+ },
+ setWorkingCase,
+ )
+ } else if (clientId) {
setAndSendDefendantToServer(
- { ...updatedLawyer, caseId: workingCase.id, defendantId },
+ { ...updatedLawyer, caseId: workingCase.id, defendantId: clientId },
setWorkingCase,
)
} else {
@@ -119,12 +187,13 @@ const DefenderInput: FC = ({
}
},
[
- defendantId,
- onDefenderNotFound,
+ onAdvocateNotFound,
lawyers,
- setAndSendDefendantToServer,
+ setAndSendCivilClaimantToServer,
workingCase,
+ civilClaimantInCivilClaimants?.caseFilesSharedWithSpokesperson,
setWorkingCase,
+ setAndSendDefendantToServer,
setAndSendCaseToServer,
],
)
@@ -132,7 +201,7 @@ const DefenderInput: FC = ({
const propertyValidations = useCallback(
(property: InputType) => {
const propertyValidation: PropertyValidation =
- property === 'defenderEmail'
+ property === 'defenderEmail' || property === 'spokespersonEmail'
? {
validations: ['email-format'],
errorMessageHandler: {
@@ -154,20 +223,30 @@ const DefenderInput: FC = ({
)
const formatUpdate = useCallback((property: InputType, value: string) => {
- return property === 'defenderEmail'
- ? {
+ switch (property) {
+ case 'defenderEmail': {
+ return {
defenderEmail: value,
}
- : {
- defenderPhoneNumber: value,
- }
+ }
+ case 'defenderPhoneNumber': {
+ return { defenderPhoneNumber: value }
+ }
+ case 'spokespersonEmail': {
+ return { spokespersonEmail: value }
+ }
+ case 'spokespersonPhoneNumber': {
+ return { spokespersonPhoneNumber: value }
+ }
+ }
}, [])
const handleLawyerPropertyChange = useCallback(
(
- defendantId: string,
+ clientId: string,
property: InputType,
value: string,
+ isCivilClaim: boolean,
setWorkingCase: Dispatch>,
) => {
let newValue = value
@@ -185,21 +264,29 @@ const DefenderInput: FC = ({
propertyValidation.errorMessageHandler.setErrorMessage,
)
- updateDefendantState(
- { ...update, caseId: workingCase.id, defendantId },
- setWorkingCase,
- )
+ if (isCivilClaim) {
+ updateCivilClaimantState(
+ { ...update, caseId: workingCase.id, civilClaimantId: clientId },
+ setWorkingCase,
+ )
+ } else {
+ updateDefendantState(
+ { ...update, caseId: workingCase.id, defendantId: clientId },
+ setWorkingCase,
+ )
+ }
},
- [formatUpdate, propertyValidations, updateDefendantState, workingCase.id],
+ [
+ formatUpdate,
+ propertyValidations,
+ updateCivilClaimantState,
+ updateDefendantState,
+ workingCase.id,
+ ],
)
const handleLawyerPropertyBlur = useCallback(
- (
- caseId: string,
- defendantId: string,
- property: InputType,
- value: string,
- ) => {
+ (caseId: string, clientId: string, property: InputType, value: string) => {
const propertyValidation = propertyValidations(property)
const update = formatUpdate(property, value)
@@ -209,24 +296,48 @@ const DefenderInput: FC = ({
propertyValidation.errorMessageHandler.setErrorMessage,
)
- updateDefendant({ ...update, caseId: workingCase.id, defendantId })
+ if (isCivilClaim) {
+ updateCivilClaimant({ ...update, caseId, civilClaimantId: clientId })
+ } else {
+ updateDefendant({ ...update, caseId, defendantId: clientId })
+ }
},
- [formatUpdate, propertyValidations, updateDefendant, workingCase.id],
+ [
+ formatUpdate,
+ isCivilClaim,
+ propertyValidations,
+ updateCivilClaimant,
+ updateDefendant,
+ ],
)
return (
<>
= ({
hasError={emailErrorMessage !== ''}
disabled={Boolean(disabled)}
onChange={(event) => {
- if (defendantId) {
+ if (clientId) {
handleLawyerPropertyChange(
- defendantId,
- 'defenderEmail',
+ clientId,
+ isCivilClaim ? 'spokespersonEmail' : 'defenderEmail',
event.target.value,
+ isCivilClaim,
setWorkingCase,
)
} else {
@@ -284,11 +405,11 @@ const DefenderInput: FC = ({
}
}}
onBlur={(event) => {
- if (defendantId) {
+ if (clientId) {
handleLawyerPropertyBlur(
workingCase.id,
- defendantId,
- 'defenderEmail',
+ clientId,
+ isCivilClaim ? 'spokespersonEmail' : 'defenderEmail',
event.target.value,
)
} else {
@@ -308,17 +429,20 @@ const DefenderInput: FC = ({
mask="999-9999"
maskPlaceholder={null}
value={
- defendantId
+ isCivilClaim
+ ? civilClaimantInCivilClaimants?.spokespersonPhoneNumber || ''
+ : clientId
? defendantInDefendants?.defenderPhoneNumber || ''
: workingCase.defenderPhoneNumber || ''
}
disabled={Boolean(disabled)}
onChange={(event) => {
- if (defendantId) {
+ if (clientId) {
handleLawyerPropertyChange(
- defendantId,
- 'defenderPhoneNumber',
+ clientId,
+ isCivilClaim ? 'spokespersonPhoneNumber' : 'defenderPhoneNumber',
event.target.value,
+ isCivilClaim,
setWorkingCase,
)
} else {
@@ -333,11 +457,11 @@ const DefenderInput: FC = ({
}
}}
onBlur={(event) => {
- if (defendantId) {
+ if (clientId) {
handleLawyerPropertyBlur(
workingCase.id,
- defendantId,
- 'defenderPhoneNumber',
+ clientId,
+ isCivilClaim ? 'spokespersonPhoneNumber' : 'defenderPhoneNumber',
event.target.value,
)
} else {
@@ -353,15 +477,17 @@ const DefenderInput: FC = ({
}}
>
@@ -370,4 +496,4 @@ const DefenderInput: FC = ({
)
}
-export default DefenderInput
+export default InputAdvocate
diff --git a/apps/judicial-system/web/src/components/Inputs/InputNationalId.tsx b/apps/judicial-system/web/src/components/Inputs/InputNationalId.tsx
index 3f4a2d1f8014..10ffb585cef0 100644
--- a/apps/judicial-system/web/src/components/Inputs/InputNationalId.tsx
+++ b/apps/judicial-system/web/src/components/Inputs/InputNationalId.tsx
@@ -75,12 +75,8 @@ const InputNationalId: FC = (props) => {
}
useEffect(() => {
- if (value === undefined) {
- return
- }
-
setErrorMessage(undefined)
- setInputValue(value)
+ setInputValue(value ?? '')
}, [value])
return (
diff --git a/apps/judicial-system/web/src/components/PdfButton/PdfButton.tsx b/apps/judicial-system/web/src/components/PdfButton/PdfButton.tsx
index 93f455330ffa..8ff7a5bd0fb0 100644
--- a/apps/judicial-system/web/src/components/PdfButton/PdfButton.tsx
+++ b/apps/judicial-system/web/src/components/PdfButton/PdfButton.tsx
@@ -7,7 +7,8 @@ import { UserContext } from '../UserProvider/UserProvider'
import * as styles from './PdfButton.css'
interface Props {
- caseId: string
+ caseId?: string
+ connectedCaseParentId?: string
title?: string | null
pdfType?:
| 'ruling'
@@ -27,6 +28,9 @@ interface Props {
const PdfButton: FC> = ({
caseId,
+ // This is used when accessing data belonging to a case which has been merged into another case.
+ // For access control purposes, the data must be accessed through the parent case.
+ connectedCaseParentId,
title,
pdfType,
disabled,
@@ -39,10 +43,14 @@ const PdfButton: FC> = ({
const { limitedAccess } = useContext(UserContext)
const handlePdfClick = async () => {
- const prefix = limitedAccess ? 'limitedAccess/' : ''
+ const prefix = `${limitedAccess ? 'limitedAccess/' : ''}${
+ connectedCaseParentId ? `mergedCase/${caseId}/` : ''
+ }`
const postfix = elementId ? `/${elementId}` : ''
const query = queryParameters ? `?${queryParameters}` : ''
- const url = `${api.apiUrl}/api/case/${caseId}/${prefix}${pdfType}${postfix}${query}`
+ const url = `${api.apiUrl}/api/case/${
+ connectedCaseParentId ?? caseId
+ }/${prefix}${pdfType}${postfix}${query}`
window.open(url, '_blank')
}
diff --git a/apps/judicial-system/web/src/components/index.ts b/apps/judicial-system/web/src/components/index.ts
index 2439fe955b45..fa0cfce3c06f 100644
--- a/apps/judicial-system/web/src/components/index.ts
+++ b/apps/judicial-system/web/src/components/index.ts
@@ -23,7 +23,6 @@ export { default as CourtRecordAccordionItem } from './AccordionItems/CourtRecor
export { default as DateTime } from './DateTime/DateTime'
export { default as Decision } from './Decision/Decision'
export { default as DefenderInfo } from './DefenderInfo/DefenderInfo'
-export { default as DefenderInput } from './DefenderInfo/DefenderInput'
export { default as DefenderNotFound } from './DefenderInfo/DefenderNotFound'
export {
default as FeatureProvider,
@@ -40,9 +39,12 @@ export { default as InfoBox } from './InfoBox/InfoBox'
export { default as BlueBoxWithIcon } from './BlueBoxWithIcon/BlueBoxWithIcon'
export { default as InfoCard } from './InfoCard/InfoCard'
export { default as InfoCardActiveIndictment } from './InfoCard/InfoCardActiveIndictment'
-export { default as InfoCardClosedIndictment } from './InfoCard/InfoCardClosedIndictment/InfoCardClosedIndictment'
+export { default as InfoCardClosedIndictment } from './InfoCard/InfoCardClosedIndictment'
export { default as CaseScheduledCard } from './BlueBoxWithIcon/CaseScheduledCard'
export { default as IndictmentCaseScheduledCard } from './BlueBoxWithIcon/IndictmentCaseScheduledCard'
+export { default as InputAdvocate } from './Inputs/InputAdvocate'
+export { default as InputName } from './Inputs/InputName'
+export { default as InputNationalId } from './Inputs/InputNationalId'
export { default as Loading } from './Loading/Loading'
export { default as Logo } from './Logo/Logo'
export { default as MarkdownWrapper } from './MarkdownWrapper/MarkdownWrapper'
diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx
index 4c407547d670..9d9383794e44 100644
--- a/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx
+++ b/apps/judicial-system/web/src/routes/Court/Indictments/Completed/Completed.tsx
@@ -3,6 +3,7 @@ import { useIntl } from 'react-intl'
import router from 'next/router'
import {
+ Accordion,
Box,
InputFileUpload,
RadioButton,
@@ -18,11 +19,13 @@ import {
FormContext,
FormFooter,
IndictmentCaseFilesList,
+ IndictmentsLawsBrokenAccordionItem,
InfoCardClosedIndictment,
Modal,
PageHeader,
PageLayout,
SectionHeading,
+ useIndictmentsLawsBroken,
} from '@island.is/judicial-system-web/src/components'
import {
CaseFileCategory,
@@ -48,6 +51,7 @@ const Completed: FC = () => {
useUploadFiles(workingCase.caseFiles)
const { handleUpload, handleRemove } = useS3Upload(workingCase.id)
const { createEventLog } = useEventLog()
+ const lawsBroken = useIndictmentsLawsBroken(workingCase)
const [modalVisible, setModalVisible] =
useState<'SENT_TO_PUBLIC_PROSECUTOR'>()
@@ -126,6 +130,10 @@ const Completed: FC = () => {
)
: true
+ const hasLawsBroken = lawsBroken.size > 0
+ const hasMergeCases =
+ workingCase.mergedCases && workingCase.mergedCases.length > 0
+
return (
{
- {workingCase.mergedCases &&
- workingCase.mergedCases.length > 0 &&
- workingCase.mergedCases.map((mergedCase) => (
-
-
-
- ))}
+ {(hasLawsBroken || hasMergeCases) && (
+
+ {hasLawsBroken && (
+
+ )}
+ {hasMergeCases && (
+
+ {workingCase.mergedCases?.map((mergedCase) => (
+
+
+
+ ))}
+
+ )}
+
+ )}
diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Defender/SelectDefender.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Defender/SelectDefender.tsx
index 411d8654c9c6..824cca080df2 100644
--- a/apps/judicial-system/web/src/routes/Court/Indictments/Defender/SelectDefender.tsx
+++ b/apps/judicial-system/web/src/routes/Court/Indictments/Defender/SelectDefender.tsx
@@ -6,9 +6,9 @@ import { capitalize } from '@island.is/judicial-system/formatters'
import { core } from '@island.is/judicial-system-web/messages'
import {
BlueBox,
- DefenderInput,
DefenderNotFound,
FormContext,
+ InputAdvocate,
} from '@island.is/judicial-system-web/src/components'
import {
Defendant,
@@ -96,10 +96,10 @@ const SelectDefender: FC = ({ defendant }) => {
large
/>
-
diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.tsx
index ce7a017ecbcb..0df16e8b0a6d 100644
--- a/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.tsx
+++ b/apps/judicial-system/web/src/routes/Court/Indictments/Overview/Overview.tsx
@@ -2,7 +2,7 @@ import { useCallback, useContext, useState } from 'react'
import { useIntl } from 'react-intl'
import { useRouter } from 'next/router'
-import { Box } from '@island.is/island-ui/core'
+import { Accordion, Box } from '@island.is/island-ui/core'
import * as constants from '@island.is/judicial-system/consts'
import { core, titles } from '@island.is/judicial-system-web/messages'
import {
@@ -101,13 +101,18 @@ const IndictmentOverview = () => {
)}
- {workingCase.mergedCases &&
- workingCase.mergedCases.length > 0 &&
- workingCase.mergedCases.map((mergedCase) => (
-
-
-
- ))}
+ {workingCase.mergedCases && workingCase.mergedCases.length > 0 && (
+
+ {workingCase.mergedCases.map((mergedCase) => (
+
+
+
+ ))}
+
+ )}
{workingCase.caseFiles && (
diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx
index 53be6c8953c9..4d13fb753f1c 100644
--- a/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx
+++ b/apps/judicial-system/web/src/routes/Court/Indictments/Subpoena/Subpoena.tsx
@@ -4,7 +4,7 @@ import router from 'next/router'
import { Box } from '@island.is/island-ui/core'
import * as constants from '@island.is/judicial-system/consts'
-import { core, titles } from '@island.is/judicial-system-web/messages'
+import { titles } from '@island.is/judicial-system-web/messages'
import {
CourtArrangements,
CourtCaseInfo,
@@ -46,11 +46,11 @@ const Subpoena: FC = () => {
} = useCourtArrangements(workingCase, setWorkingCase, 'arraignmentDate')
const { sendNotification } = useCase()
- const isArraignmentDone = Boolean(workingCase.indictmentDecision)
+ const isArraignmentScheduled = Boolean(workingCase.arraignmentDate)
const handleNavigationTo = useCallback(
async (destination: keyof stepValidationsType) => {
- if (isArraignmentDone) {
+ if (isArraignmentScheduled) {
router.push(`${destination}/${workingCase.id}`)
return
}
@@ -89,7 +89,7 @@ const Subpoena: FC = () => {
router.push(`${destination}/${workingCase.id}`)
},
[
- isArraignmentDone,
+ isArraignmentScheduled,
sendCourtDateToServer,
workingCase.defendants,
workingCase.notifications,
@@ -134,8 +134,8 @@ const Subpoena: FC = () => {
handleCourtDateChange={handleCourtDateChange}
handleCourtRoomChange={handleCourtRoomChange}
courtDate={workingCase.arraignmentDate}
- dateTimeDisabled={isArraignmentDone}
- courtRoomDisabled={isArraignmentDone}
+ dateTimeDisabled={isArraignmentScheduled}
+ courtRoomDisabled={isArraignmentScheduled}
courtRoomRequired
/>
@@ -169,14 +169,14 @@ const Subpoena: FC = () => {
previousUrl={`${constants.INDICTMENTS_RECEPTION_AND_ASSIGNMENT_ROUTE}/${workingCase.id}`}
nextIsLoading={isLoadingWorkingCase}
onNextButtonClick={() => {
- if (isArraignmentDone) {
+ if (isArraignmentScheduled) {
router.push(
`${constants.INDICTMENTS_DEFENDER_ROUTE}/${workingCase.id}`,
)
} else setNavigateTo(constants.INDICTMENTS_DEFENDER_ROUTE)
}}
nextButtonText={
- isArraignmentDone
+ isArraignmentScheduled
? undefined
: formatMessage(strings.nextButtonText)
}
diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Summary/Summary.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Summary/Summary.tsx
index f6128bdd3cfe..d1186e033ce4 100644
--- a/apps/judicial-system/web/src/routes/Court/Indictments/Summary/Summary.tsx
+++ b/apps/judicial-system/web/src/routes/Court/Indictments/Summary/Summary.tsx
@@ -2,7 +2,7 @@ import { FC, useContext, useState } from 'react'
import { useIntl } from 'react-intl'
import router from 'next/router'
-import { Box, Text } from '@island.is/island-ui/core'
+import { Accordion, Box, Text } from '@island.is/island-ui/core'
import * as constants from '@island.is/judicial-system/consts'
import { core } from '@island.is/judicial-system-web/messages'
import {
@@ -126,13 +126,18 @@ const Summary: FC = () => {
- {workingCase.mergedCases &&
- workingCase.mergedCases.length > 0 &&
- workingCase.mergedCases.map((mergedCase) => (
-
-
-
- ))}
+ {workingCase.mergedCases && workingCase.mergedCases.length > 0 && (
+
+ {workingCase.mergedCases.map((mergedCase) => (
+
+
+
+ ))}
+
+ )}
{(rulingFiles.length > 0 || courtRecordFiles.length > 0) && (
@@ -140,18 +145,10 @@ const Summary: FC = () => {
{formatMessage(strings.caseFilesSubtitleRuling)}
{rulingFiles.length > 0 && (
-
+
)}
{courtRecordFiles.length > 0 && (
-
+
)}
)}
diff --git a/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.tsx b/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.tsx
index a0f765f07198..e54977b11363 100644
--- a/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.tsx
+++ b/apps/judicial-system/web/src/routes/Prison/IndictmentOverview/IndictmentOverview.tsx
@@ -73,7 +73,6 @@ const IndictmentOverview = () => {
{formatMessage(strings.verdictTitle)}
{
const { workingCase, isLoadingWorkingCase, caseNotFound } =
useContext(FormContext)
+
+ const caseFiles = useMemo(() => {
+ return (
+ workingCase.caseFiles?.filter(
+ (caseFile) => caseFile.category === CaseFileCategory.CASE_FILE_RECORD,
+ ) ?? []
+ )
+ }, [workingCase.caseFiles])
+
const { formatMessage } = useIntl()
const [editCount, setEditCount] = useState(0)
@@ -57,24 +66,20 @@ const CaseFile = () => {
- {workingCase.policeCaseNumbers?.map((policeCaseNumber, index) => (
-
- caseFile.policeCaseNumber === policeCaseNumber &&
- caseFile.category === CaseFileCategory.CASE_FILE_RECORD,
- ) ?? []
- }
- subtypes={workingCase.indictmentSubtypes}
- crimeScenes={workingCase.crimeScenes}
- setEditCount={setEditCount}
- />
- ))}
+ {workingCase.policeCaseNumbers?.map((policeCaseNumber, index) => {
+ return (
+
+ )
+ })}
diff --git a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Overview/Overview.tsx b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Overview/Overview.tsx
index 5ed718188275..3c13b47bef12 100644
--- a/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Overview/Overview.tsx
+++ b/apps/judicial-system/web/src/routes/Prosecutor/Indictments/Overview/Overview.tsx
@@ -4,6 +4,7 @@ import { AnimatePresence } from 'framer-motion'
import { useRouter } from 'next/router'
import {
+ Accordion,
AlertMessage,
Box,
Button,
@@ -15,6 +16,7 @@ import * as constants from '@island.is/judicial-system/consts'
import { core, errors, titles } from '@island.is/judicial-system-web/messages'
import {
BlueBox,
+ ConnectedCaseFilesAccordionItem,
FormContentContainer,
FormContext,
FormFooter,
@@ -153,6 +155,10 @@ const Overview: FC = () => {
router.push(constants.CASES_ROUTE)
}
+ const hasLawsBroken = lawsBroken.size > 0
+ const hasMergeCases =
+ workingCase.mergedCases && workingCase.mergedCases.length > 0
+
return (
{
- {lawsBroken.size > 0 && (
+ {(hasLawsBroken || hasMergeCases) && (
-
+ {hasLawsBroken && (
+
+ )}
+ {hasMergeCases && (
+
+ {workingCase.mergedCases?.map((mergedCase) => (
+
+
+
+ ))}
+
+ )}
)}
{
refreshCase,
} = useContext(FormContext)
const { updateCase, transitionCase, setAndSendCaseToServer } = useCase()
- const { handleRemove } = useS3Upload(workingCase.id)
const { formatMessage } = useIntl()
const { updateDefendant, updateDefendantState } = useDefendants()
+ const {
+ updateCivilClaimant,
+ updateCivilClaimantState,
+ createCivilClaimant,
+ deleteCivilClaimant,
+ } = useCivilClaimants()
const router = useRouter()
const isTrafficViolationCaseCheck = isTrafficViolationCase(workingCase)
-
- const [hasCivilClaimsChoice, setHasCivilClaimsChoice] = useState()
+ const [civilClaimantNationalIdUpdate, setCivilClaimantNationalIdUpdate] =
+ useState<{ nationalId: string; civilClaimantId: string }>()
+ const [hasCivilClaimantChoice, setHasCivilClaimantChoice] =
+ useState()
+ const [nationalIdNotFound, setNationalIdNotFound] = useState(false)
const initialize = useCallback(async () => {
if (!workingCase.court) {
@@ -88,21 +107,57 @@ const Processing: FC = () => {
},
[router, setWorkingCase, transitionCase, workingCase],
)
- const stepIsValid = isProcessingStepValidIndictments(workingCase)
+
+ const { personData } = useNationalRegistry(
+ civilClaimantNationalIdUpdate?.nationalId,
+ )
+
+ const stepIsValid =
+ isProcessingStepValidIndictments(workingCase) &&
+ nationalIdNotFound === false
const handleUpdateDefendant = useCallback(
(updatedDefendant: UpdateDefendantInput) => {
updateDefendantState(updatedDefendant, setWorkingCase)
+ updateDefendant(updatedDefendant)
+ },
+ [updateDefendantState, setWorkingCase, updateDefendant],
+ )
- if (workingCase.id) {
- updateDefendant(updatedDefendant)
- }
+ const handleUpdateCivilClaimant = useCallback(
+ (updatedCivilClaimant: UpdateCivilClaimantInput) => {
+ updateCivilClaimantState(updatedCivilClaimant, setWorkingCase)
+ updateCivilClaimant(updatedCivilClaimant)
},
- [updateDefendantState, setWorkingCase, workingCase.id, updateDefendant],
+ [updateCivilClaimant, setWorkingCase, updateCivilClaimantState],
)
+ const handleCreateCivilClaimantClick = async () => {
+ addCivilClaimant()
+
+ window.scrollTo(0, document.body.scrollHeight)
+ }
+
+ const addCivilClaimant = useCallback(async () => {
+ const civilClaimantId = await createCivilClaimant({
+ caseId: workingCase.id,
+ })
+
+ setWorkingCase((prevWorkingCase) => ({
+ ...prevWorkingCase,
+ civilClaimants: prevWorkingCase.civilClaimants && [
+ ...prevWorkingCase.civilClaimants,
+ {
+ id: civilClaimantId,
+ name: '',
+ nationalId: '',
+ } as CivilClaimant,
+ ],
+ }))
+ }, [createCivilClaimant, setWorkingCase, workingCase.id])
+
const handleHasCivilClaimsChange = async (hasCivilClaims: boolean) => {
- setHasCivilClaimsChoice(hasCivilClaims)
+ setHasCivilClaimantChoice(hasCivilClaims)
setAndSendCaseToServer(
[{ hasCivilClaims, force: true }],
@@ -110,26 +165,114 @@ const Processing: FC = () => {
setWorkingCase,
)
- if (hasCivilClaims === false) {
- const civilClaims = workingCase.caseFiles?.filter(
- (caseFile) => caseFile.category === CaseFileCategory.CIVIL_CLAIM,
- )
+ if (hasCivilClaims) {
+ addCivilClaimant()
+ } else {
+ removeAllCivilClaimants()
+ }
+ }
- if (!civilClaims) {
+ const handleCivilClaimantNationalIdBlur = async (
+ nationalId: string,
+ noNationalId?: boolean | null,
+ civilClaimantId?: string | null,
+ ) => {
+ if (!civilClaimantId) {
+ return
+ }
+
+ if (noNationalId) {
+ handleUpdateCivilClaimant({
+ caseId: workingCase.id,
+ civilClaimantId,
+ nationalId,
+ })
+ } else {
+ const cleanNationalId = nationalId ? nationalId.replace('-', '') : ''
+ setCivilClaimantNationalIdUpdate({
+ nationalId: cleanNationalId,
+ civilClaimantId,
+ })
+ }
+ }
+
+ const handleCivilClaimantNameBlur = async (
+ name: string,
+ civilClaimantId?: string | null,
+ ) => {
+ if (!civilClaimantId) {
+ return
+ }
+
+ updateCivilClaimant({ name, civilClaimantId, caseId: workingCase.id })
+ }
+
+ const removeAllCivilClaimants = useCallback(async () => {
+ const promises: Promise[] = []
+
+ if (!workingCase.civilClaimants) {
+ return
+ }
+
+ for (const civilClaimant of workingCase.civilClaimants) {
+ if (!civilClaimant.id) {
return
}
- setAndSendCaseToServer(
- [{ civilDemands: null, force: true }],
- workingCase,
- setWorkingCase,
- )
+ promises.push(deleteCivilClaimant(workingCase.id, civilClaimant.id))
+ }
- for (const civilClaim of civilClaims) {
- handleRemove(civilClaim as UploadFile)
+ const allCivilClaimantsDeleted = await Promise.all(promises)
+
+ if (allCivilClaimantsDeleted.every((deleted) => deleted)) {
+ setWorkingCase((prev) => ({ ...prev, civilClaimants: [] }))
+ }
+ }, [
+ deleteCivilClaimant,
+ setWorkingCase,
+ workingCase.civilClaimants,
+ workingCase.id,
+ ])
+
+ const removeCivilClaimantById = useCallback(
+ async (caseId: string, civilClaimantId?: string | null) => {
+ if (!civilClaimantId) {
+ return
+ }
+
+ const deleteSuccess = await deleteCivilClaimant(caseId, civilClaimantId)
+
+ if (!deleteSuccess) {
+ return
}
+
+ const newCivilClaimants = workingCase.civilClaimants?.filter(
+ (civilClaimant) => civilClaimant.id !== civilClaimantId,
+ )
+
+ setWorkingCase((prev) => ({ ...prev, civilClaimants: newCivilClaimants }))
+ },
+ [deleteCivilClaimant, setWorkingCase, workingCase.civilClaimants],
+ )
+
+ useEffect(() => {
+ if (!personData || !personData.items || personData.items.length === 0) {
+ setNationalIdNotFound(true)
+ return
+ }
+
+ setNationalIdNotFound(false)
+ const update = {
+ caseId: workingCase.id,
+ civilClaimantId: civilClaimantNationalIdUpdate?.civilClaimantId || '',
+ name: personData?.items[0].name,
+ nationalId: personData.items[0].kennitala,
}
- }
+
+ handleUpdateCivilClaimant(update)
+ // We want this hook to run exclusively when personData changes.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [personData])
return (
{
>
{
handleHasCivilClaimsChange(true)}
checked={
- hasCivilClaimsChoice === true ||
- (hasCivilClaimsChoice === undefined &&
+ hasCivilClaimantChoice === true ||
+ (hasCivilClaimantChoice === undefined &&
workingCase.hasCivilClaims === true)
}
/>
handleHasCivilClaimsChange(false)}
checked={
- hasCivilClaimsChoice === false ||
- (hasCivilClaimsChoice === undefined &&
+ hasCivilClaimantChoice === false ||
+ (hasCivilClaimantChoice === undefined &&
workingCase.hasCivilClaims === false)
}
/>
@@ -276,6 +419,242 @@ const Processing: FC = () => {
+ {workingCase.hasCivilClaims && (
+ <>
+
+ {workingCase.civilClaimants?.map((civilClaimant, index) => (
+
+
+ {index > 0 && (
+
+
+
+ )}
+
+ {
+ handleUpdateCivilClaimant({
+ caseId: workingCase.id,
+ civilClaimantId: civilClaimant.id,
+ nationalId: null,
+ noNationalId: !civilClaimant.noNationalId,
+ })
+ }}
+ backgroundColor="white"
+ large
+ filled
+ />
+
+
+ {
+ if (val.length < 11) {
+ setNationalIdNotFound(false)
+ } else if (val.length === 11) {
+ handleCivilClaimantNationalIdBlur(
+ val,
+ civilClaimant.noNationalId,
+ civilClaimant.id,
+ )
+ }
+
+ updateCivilClaimantState(
+ {
+ caseId: workingCase.id,
+ civilClaimantId: civilClaimant.id ?? '',
+ nationalId: val,
+ },
+ setWorkingCase,
+ )
+ }}
+ onBlur={(val) =>
+ handleCivilClaimantNationalIdBlur(
+ val,
+ civilClaimant.noNationalId,
+ civilClaimant.id,
+ )
+ }
+ />
+ {civilClaimant.nationalId?.length === 11 &&
+ nationalIdNotFound && (
+
+ {formatMessage(
+ core.nationalIdNotFoundInNationalRegistry,
+ )}
+
+ )}
+
+
+ updateCivilClaimantState(
+ {
+ caseId: workingCase.id,
+ civilClaimantId: civilClaimant.id ?? '',
+ name: val,
+ },
+ setWorkingCase,
+ )
+ }
+ onBlur={(val) =>
+ handleCivilClaimantNameBlur(val, civilClaimant.id)
+ }
+ required
+ />
+
+
+
+ {civilClaimant.hasSpokesperson && (
+ <>
+
+
+
+ handleUpdateCivilClaimant({
+ caseId: workingCase.id,
+ civilClaimantId: civilClaimant.id,
+ spokespersonIsLawyer: true,
+ })
+ }
+ checked={Boolean(
+ civilClaimant.spokespersonIsLawyer,
+ )}
+ />
+
+
+
+ handleUpdateCivilClaimant({
+ caseId: workingCase.id,
+ civilClaimantId: civilClaimant.id,
+ spokespersonIsLawyer: false,
+ })
+ }
+ checked={
+ civilClaimant.spokespersonIsLawyer === false
+ }
+ />
+
+
+
+
+
+ {
+ handleUpdateCivilClaimant({
+ caseId: workingCase.id,
+ civilClaimantId: civilClaimant.id,
+ caseFilesSharedWithSpokesperson:
+ !civilClaimant.caseFilesSharedWithSpokesperson,
+ })
+ }}
+ disabled={
+ civilClaimant.spokespersonIsLawyer === null ||
+ civilClaimant.spokespersonIsLawyer === undefined
+ }
+ tooltip={formatMessage(
+ strings.civilClaimantShareFilesWithDefenderTooltip,
+ )}
+ backgroundColor="white"
+ large
+ filled
+ />
+ >
+ )}
+
+
+ ))}
+
+
+
+ >
+ )}
{
[formatMessage, router, setWorkingCase, transitionCase, workingCase],
)
- const stepIsValid =
- isHearingArrangementsStepValidRC(workingCase) || isTransitioningCase
+ const stepIsValid = isHearingArrangementsStepValidRC(workingCase)
return (
{
[router, workingCase.id],
)
+ const hasLawsBroken = lawsBroken.size > 0
+ const hasMergeCases =
+ workingCase.mergedCases && workingCase.mergedCases.length > 0
+
return (
{
)}
- {lawsBroken.size > 0 && (
+ {(hasLawsBroken || hasMergeCases) && (
-
+ {hasLawsBroken && (
+
+ )}
+ {hasMergeCases && (
+
+ {workingCase.mergedCases?.map((mergedCase) => (
+
+
+
+ ))}
+
+ )}
)}
{workingCase.caseFiles && (
diff --git a/apps/judicial-system/web/src/utils/hooks/index.ts b/apps/judicial-system/web/src/utils/hooks/index.ts
index 5b45d181ec23..3c9d8a29cec0 100644
--- a/apps/judicial-system/web/src/utils/hooks/index.ts
+++ b/apps/judicial-system/web/src/utils/hooks/index.ts
@@ -28,3 +28,4 @@ export {
export { default as useSections } from './useSections'
export { default as useCaseList } from './useCaseList'
export { default as useNationalRegistry } from './useNationalRegistry'
+export { default as useCivilClaimants } from './useCivilClaimants'
diff --git a/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/createCivilClaimant.graphql b/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/createCivilClaimant.graphql
new file mode 100644
index 000000000000..293fac4ddc6f
--- /dev/null
+++ b/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/createCivilClaimant.graphql
@@ -0,0 +1,5 @@
+mutation CreateCivilClaimant($input: CreateCivilClaimantInput!) {
+ createCivilClaimant(input: $input) {
+ id
+ }
+}
diff --git a/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/deleteCivilClaimant.graphql b/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/deleteCivilClaimant.graphql
new file mode 100644
index 000000000000..8e361f5855dc
--- /dev/null
+++ b/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/deleteCivilClaimant.graphql
@@ -0,0 +1,5 @@
+mutation DeleteCivilClaimant($input: DeleteCivilClaimantInput!) {
+ deleteCivilClaimant(input: $input) {
+ deleted
+ }
+}
diff --git a/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/index.ts b/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/index.ts
new file mode 100644
index 000000000000..9ceb164b2be2
--- /dev/null
+++ b/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/index.ts
@@ -0,0 +1,133 @@
+import { Dispatch, SetStateAction, useCallback } from 'react'
+import { useIntl } from 'react-intl'
+
+import { toast } from '@island.is/island-ui/core'
+import { errors } from '@island.is/judicial-system-web/messages'
+import {
+ CivilClaimant,
+ CreateCivilClaimantInput,
+ UpdateCivilClaimantInput,
+} from '@island.is/judicial-system-web/src/graphql/schema'
+import { TempCase as Case } from '@island.is/judicial-system-web/src/types'
+
+import { useCreateCivilClaimantMutation } from './createCivilClaimant.generated'
+import { useDeleteCivilClaimantMutation } from './deleteCivilClaimant.generated'
+import { useUpdateCivilClaimantMutation } from './updateCivilClaimant.generated'
+
+const useCivilClaimants = () => {
+ const { formatMessage } = useIntl()
+
+ const [createCivilClaimantMutation, { loading: isCreatingCivilClaimant }] =
+ useCreateCivilClaimantMutation()
+ const [deleteCivilClaimantMutation] = useDeleteCivilClaimantMutation()
+ const [updateCivilClaimantMutation] = useUpdateCivilClaimantMutation()
+
+ const createCivilClaimant = useCallback(
+ async (civilClaimant: CreateCivilClaimantInput) => {
+ try {
+ if (!isCreatingCivilClaimant) {
+ const { data } = await createCivilClaimantMutation({
+ variables: {
+ input: civilClaimant,
+ },
+ })
+
+ if (data) {
+ return data.createCivilClaimant?.id
+ }
+ }
+ return null
+ } catch (error) {
+ toast.error(formatMessage(errors.createCivilClaimant))
+ return null
+ }
+ },
+ [createCivilClaimantMutation, formatMessage, isCreatingCivilClaimant],
+ )
+
+ const deleteCivilClaimant = useCallback(
+ async (caseId: string, civilClaimantId: string) => {
+ try {
+ const { data } = await deleteCivilClaimantMutation({
+ variables: { input: { caseId, civilClaimantId } },
+ })
+
+ return Boolean(data?.deleteCivilClaimant.deleted)
+ } catch (error) {
+ toast.error(formatMessage(errors.deleteCivilClaimant))
+ return false
+ }
+ },
+ [deleteCivilClaimantMutation, formatMessage],
+ )
+
+ const updateCivilClaimant = useCallback(
+ async (updateCivilClaimant: UpdateCivilClaimantInput) => {
+ try {
+ const { data } = await updateCivilClaimantMutation({
+ variables: {
+ input: updateCivilClaimant,
+ },
+ })
+
+ return Boolean(data)
+ } catch (error) {
+ toast.error(formatMessage(errors.updateCivilClaimant))
+ return false
+ }
+ },
+ [formatMessage, updateCivilClaimantMutation],
+ )
+
+ const updateCivilClaimantState = useCallback(
+ (
+ update: UpdateCivilClaimantInput,
+ setWorkingCase: Dispatch>,
+ ) => {
+ setWorkingCase((prevWorkingCase: Case) => {
+ if (!prevWorkingCase.civilClaimants) {
+ return prevWorkingCase
+ }
+ const indexOfCivilClaimantToUpdate =
+ prevWorkingCase.civilClaimants.findIndex(
+ (civilClaimant) => civilClaimant.id === update.civilClaimantId,
+ )
+
+ if (indexOfCivilClaimantToUpdate === -1) {
+ return prevWorkingCase
+ } else {
+ const newCivilClaimants = [...prevWorkingCase.civilClaimants]
+
+ newCivilClaimants[indexOfCivilClaimantToUpdate] = {
+ ...newCivilClaimants[indexOfCivilClaimantToUpdate],
+ ...update,
+ } as CivilClaimant
+
+ return { ...prevWorkingCase, civilClaimants: newCivilClaimants }
+ }
+ })
+ },
+ [],
+ )
+
+ const setAndSendCivilClaimantToServer = useCallback(
+ (
+ update: UpdateCivilClaimantInput,
+ setWorkingCase: Dispatch>,
+ ) => {
+ updateCivilClaimantState(update, setWorkingCase)
+ updateCivilClaimant(update)
+ },
+ [updateCivilClaimant, updateCivilClaimantState],
+ )
+
+ return {
+ createCivilClaimant,
+ deleteCivilClaimant,
+ updateCivilClaimant,
+ updateCivilClaimantState,
+ setAndSendCivilClaimantToServer,
+ }
+}
+
+export default useCivilClaimants
diff --git a/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/updateCivilClaimant.graphql b/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/updateCivilClaimant.graphql
new file mode 100644
index 000000000000..6b589542eb24
--- /dev/null
+++ b/apps/judicial-system/web/src/utils/hooks/useCivilClaimants/updateCivilClaimant.graphql
@@ -0,0 +1,5 @@
+mutation UpdateCivilClaimant($input: UpdateCivilClaimantInput!) {
+ updateCivilClaimant(input: $input) {
+ id
+ }
+}
diff --git a/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts b/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts
index 7b14e6eecd68..66987e7b513b 100644
--- a/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts
+++ b/apps/judicial-system/web/src/utils/hooks/useDefendants/index.ts
@@ -50,13 +50,10 @@ const useDefendants = () => {
variables: { input: { caseId, defendantId } },
})
- if (data?.deleteDefendant?.deleted) {
- return true
- } else {
- return false
- }
+ return Boolean(data?.deleteDefendant?.deleted)
} catch (error) {
- formatMessage(errors.deleteDefendant)
+ toast.error(formatMessage(errors.deleteDefendant))
+ return false
}
},
[deleteDefendantMutation, formatMessage],
@@ -71,13 +68,10 @@ const useDefendants = () => {
},
})
- if (data) {
- return true
- } else {
- return false
- }
+ return Boolean(data)
} catch (error) {
toast.error(formatMessage(errors.updateDefendant))
+ return false
}
},
[formatMessage, updateDefendantMutation],
diff --git a/apps/judicial-system/web/src/utils/hooks/useFileList/index.ts b/apps/judicial-system/web/src/utils/hooks/useFileList/index.ts
index 475d9bd11bca..0ab7be7464ed 100644
--- a/apps/judicial-system/web/src/utils/hooks/useFileList/index.ts
+++ b/apps/judicial-system/web/src/utils/hooks/useFileList/index.ts
@@ -14,9 +14,10 @@ import { useLimitedAccessGetSignedUrlLazyQuery } from './limitedAccessGetSigendU
interface Parameters {
caseId: string
+ connectedCaseParentId?: string
}
-const useFileList = ({ caseId }: Parameters) => {
+const useFileList = ({ caseId, connectedCaseParentId }: Parameters) => {
const { limitedAccess } = useContext(UserContext)
const { setWorkingCase } = useContext(FormContext)
const { formatMessage } = useIntl()
@@ -100,9 +101,23 @@ const useFileList = ({ caseId }: Parameters) => {
() => (fileId: string) => {
const query = limitedAccess ? limitedAccessGetSignedUrl : getSignedUrl
- query({ variables: { input: { id: fileId, caseId } } })
+ query({
+ variables: {
+ input: {
+ id: fileId,
+ caseId: connectedCaseParentId ?? caseId,
+ mergedCaseId: connectedCaseParentId && caseId,
+ },
+ },
+ })
},
- [caseId, getSignedUrl, limitedAccess, limitedAccessGetSignedUrl],
+ [
+ caseId,
+ connectedCaseParentId,
+ getSignedUrl,
+ limitedAccess,
+ limitedAccessGetSignedUrl,
+ ],
)
const dismissFileNotFound = () => {
diff --git a/apps/judicial-system/web/src/utils/validate.ts b/apps/judicial-system/web/src/utils/validate.ts
index dae538e76314..b2162dc1c6da 100644
--- a/apps/judicial-system/web/src/utils/validate.ts
+++ b/apps/judicial-system/web/src/utils/validate.ts
@@ -271,10 +271,21 @@ export const isProcessingStepValidIndictments = (
workingCase.hasCivilClaims !== null &&
workingCase.hasCivilClaims !== undefined
+ const allCivilClaimantsAreValid = workingCase.hasCivilClaims
+ ? workingCase.civilClaimants?.every(
+ (civilClaimant) =>
+ civilClaimant.name &&
+ (civilClaimant.noNationalId ||
+ (civilClaimant.nationalId &&
+ civilClaimant.nationalId.replace('-', '').length === 10)),
+ )
+ : true
+
return Boolean(
workingCase.prosecutor &&
workingCase.court &&
hasCivilClaimSelected &&
+ allCivilClaimantsAreValid &&
defendantsAreValid(),
)
}
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 fe1fe79e2480..fff13b8131cc 100644
--- a/apps/services/auth/admin-api/infra/auth-admin-api.ts
+++ b/apps/services/auth/admin-api/infra/auth-admin-api.ts
@@ -59,14 +59,29 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-admin-api'> => {
prod: 'IS/GOV/5402696029/Skatturinn/ft-v1',
},
COMPANY_REGISTRY_REDIS_NODES: REDIS_NODE_CONFIG,
+ SYSLUMENN_HOST: {
+ dev: 'https://api.syslumenn.is/staging',
+ staging: 'https://api.syslumenn.is/staging',
+ prod: 'https://api.syslumenn.is',
+ },
+ SYSLUMENN_TIMEOUT: '3000',
+ ZENDESK_CONTACT_FORM_SUBDOMAIN: {
+ prod: 'digitaliceland',
+ staging: 'digitaliceland',
+ dev: 'digitaliceland',
+ },
})
.secrets({
+ ZENDESK_CONTACT_FORM_EMAIL: '/k8s/api/ZENDESK_CONTACT_FORM_EMAIL',
+ ZENDESK_CONTACT_FORM_TOKEN: '/k8s/api/ZENDESK_CONTACT_FORM_TOKEN',
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',
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME',
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD',
})
.xroad(Base, Client, RskProcuring)
.ingress({
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 b409548fb8a2..982ab803273f 100644
--- a/apps/services/auth/admin-api/src/app/app.module.ts
+++ b/apps/services/auth/admin-api/src/app/app.module.ts
@@ -8,7 +8,13 @@ import {
SequelizeConfigService,
} from '@island.is/auth-api-lib'
import { AuthModule } from '@island.is/auth-nest-tools'
+import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships'
+import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2'
+import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry'
+import { SyslumennClientConfig } from '@island.is/clients/syslumenn'
import { AuditModule } from '@island.is/nest/audit'
+import { IdsClientConfig, XRoadConfig } from '@island.is/nest/config'
+import { FeatureFlagConfig } from '@island.is/nest/feature-flags'
import { ProblemModule } from '@island.is/nest/problem'
import { environment } from '../environments'
@@ -21,16 +27,11 @@ import { ResourcesModule } from './modules/resources/resources.module'
import { TranslationModule } from './modules/translation/translation.module'
import { UsersModule } from './modules/users/users.module'
import { ClientsModule as ClientsV2Module } from './v2/clients/clients.module'
+import { DelegationAdminModule } from './v2/delegations/delegation-admin.module'
+import { ProvidersModule } from './v2/providers/providers.module'
+import { ScopesModule } from './v2/scopes/scopes.module'
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: [
@@ -64,6 +65,7 @@ import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry'
FeatureFlagConfig,
XRoadConfig,
IdsClientConfig,
+ SyslumennClientConfig,
],
envFilePath: ['.env', '.env.secret'],
}),
diff --git a/apps/services/auth/admin-api/src/app/v2/clients/me-clients.controller.ts b/apps/services/auth/admin-api/src/app/v2/clients/me-clients.controller.ts
index 5bb808ac4fd0..649e2ba61991 100644
--- a/apps/services/auth/admin-api/src/app/v2/clients/me-clients.controller.ts
+++ b/apps/services/auth/admin-api/src/app/v2/clients/me-clients.controller.ts
@@ -7,6 +7,7 @@ import {
Post,
UseGuards,
Delete,
+ Query,
} from '@nestjs/common'
import { ApiSecurity, ApiTags } from '@nestjs/swagger'
@@ -74,7 +75,7 @@ export class MeClientsController {
@CurrentUser() user: User,
@Param('tenantId') tenantId: string,
@Param('clientId') clientId: string,
- @Param('includeArchived') includeArchived?: boolean,
+ @Query('includeArchived') includeArchived?: boolean,
): Promise {
return this.clientsService.findByTenantIdAndClientId(
tenantId,
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
index 1889f4886baf..4ab51563c17a 100644
--- 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
@@ -1,9 +1,11 @@
import {
+ Body,
Controller,
Delete,
Get,
Headers,
Param,
+ Post,
UseGuards,
} from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger'
@@ -16,8 +18,10 @@ import {
User,
} from '@island.is/auth-nest-tools'
import {
+ CreatePaperDelegationDto,
DelegationAdminCustomDto,
DelegationAdminCustomService,
+ DelegationDTO,
} from '@island.is/auth-api-lib'
import { Documentation } from '@island.is/nest/swagger'
import { Audit, AuditService } from '@island.is/nest/audit'
@@ -65,6 +69,28 @@ export class DelegationAdminController {
)
}
+ @Post()
+ @Scopes(DelegationAdminScopes.admin)
+ @Documentation({
+ response: { status: 201, type: DelegationDTO },
+ })
+ create(
+ @CurrentUser() user: User,
+ @Body() delegation: CreatePaperDelegationDto,
+ ): Promise {
+ return this.auditService.auditPromise(
+ {
+ auth: user,
+ namespace,
+ action: 'create',
+ resources: (result) => {
+ return result?.id ?? undefined
+ },
+ },
+ this.delegationAdminService.createDelegation(user, delegation),
+ )
+ }
+
@Delete(':delegationId')
@Scopes(DelegationAdminScopes.admin)
@Documentation({
diff --git a/apps/services/auth/admin-api/src/app/v2/delegations/test/delegation-admin.auth.spec.ts b/apps/services/auth/admin-api/src/app/v2/delegations/test/delegation-admin.auth.spec.ts
new file mode 100644
index 000000000000..f97758aaf310
--- /dev/null
+++ b/apps/services/auth/admin-api/src/app/v2/delegations/test/delegation-admin.auth.spec.ts
@@ -0,0 +1,135 @@
+import request from 'supertest'
+
+import {
+ getRequestMethod,
+ setupApp,
+ setupAppWithoutAuth,
+ TestApp,
+ TestEndpointOptions,
+} from '@island.is/testing/nest'
+import { User } from '@island.is/auth-nest-tools'
+import { FixtureFactory } from '@island.is/services/auth/testing'
+import { createCurrentUser } from '@island.is/testing/fixtures'
+import { DelegationAdminScopes } from '@island.is/auth/scopes'
+import { SequelizeConfigService } from '@island.is/auth-api-lib'
+
+import { AppModule } from '../../../app.module'
+
+describe('withoutAuth and permissions', () => {
+ async function formatUrl(app: TestApp, endpoint: string, user?: User) {
+ if (!endpoint.includes(':delegation')) {
+ return endpoint
+ }
+ const factory = new FixtureFactory(app)
+ const domain = await factory.createDomain({
+ name: 'd1',
+ apiScopes: [{ name: 's1' }],
+ })
+ const delegation = await factory.createCustomDelegation({
+ fromNationalId: user?.nationalId,
+ domainName: domain.name,
+ scopes: [{ scopeName: 's1' }],
+ })
+ return endpoint.replace(':delegation', encodeURIComponent(delegation.id))
+ }
+
+ it.each`
+ method | endpoint
+ ${'GET'} | ${'/delegation-admin'}
+ ${'DELETE'} | ${'/delegation-admin/:delegation'}
+ `(
+ '$method $endpoint should return 401 when user is not authenticated',
+ async ({ method, endpoint }) => {
+ // Arrange
+ const app = await setupAppWithoutAuth({
+ AppModule,
+ SequelizeConfigService,
+ dbType: 'postgres',
+ })
+ const server = request(app.getHttpServer())
+ const url = await formatUrl(app, endpoint)
+
+ // Act
+ const res = await getRequestMethod(server, method)(url)
+
+ // Assert
+ expect(res.status).toEqual(401)
+ expect(res.body).toMatchObject({
+ status: 401,
+ type: 'https://httpstatuses.org/401',
+ title: 'Unauthorized',
+ })
+ },
+ )
+
+ it.each`
+ method | endpoint
+ ${'GET'} | ${'/delegation-admin'}
+ ${'DELETE'} | ${'/delegation-admin/:delegation'}
+ `(
+ '$method $endpoint should return 403 Forbidden when user does not have the correct scope',
+ async ({ method, endpoint }: TestEndpointOptions) => {
+ // Arrange
+ const user = createCurrentUser()
+ const app = await setupApp({
+ AppModule,
+ SequelizeConfigService,
+ user,
+ dbType: 'postgres',
+ })
+ const server = request(app.getHttpServer())
+ const url = await formatUrl(app, endpoint, user)
+
+ // Act
+ const res = await getRequestMethod(server, method)(url)
+
+ // Assert
+ expect(res.status).toEqual(403)
+ expect(res.body).toMatchObject({
+ status: 403,
+ type: 'https://httpstatuses.org/403',
+ title: 'Forbidden',
+ detail: 'Forbidden resource',
+ })
+
+ // CleanUp
+ app.cleanUp()
+ },
+ )
+
+ it.each`
+ method | endpoint
+ ${'DELETE'} | ${'/delegation-admin/:delegation'}
+ `(
+ '$method $endpoint should return 403 Forbidden when user does not have the admin scope',
+ async ({ method, endpoint }: TestEndpointOptions) => {
+ // Arrange
+ const user = createCurrentUser({
+ scope: [DelegationAdminScopes.read],
+ })
+ const app = await setupApp({
+ AppModule,
+ SequelizeConfigService,
+ user,
+ dbType: 'postgres',
+ })
+ const server = request(app.getHttpServer())
+ const url = await formatUrl(app, endpoint, user)
+
+ // Act
+ const res = await getRequestMethod(server, method)(url)
+
+ // Assert
+ expect(res.status).toEqual(403)
+ expect(res.body).toMatchObject({
+ status: 403,
+ type: 'https://httpstatuses.org/403',
+ title: 'Forbidden',
+ detail: 'Forbidden resource',
+ })
+
+ // CleanUp
+ app.cleanUp()
+ },
+ )
+})
diff --git a/apps/services/auth/admin-api/src/app/v2/delegations/test/delegation-admin.spec.ts b/apps/services/auth/admin-api/src/app/v2/delegations/test/delegation-admin.spec.ts
new file mode 100644
index 000000000000..718b6427b800
--- /dev/null
+++ b/apps/services/auth/admin-api/src/app/v2/delegations/test/delegation-admin.spec.ts
@@ -0,0 +1,332 @@
+import request from 'supertest'
+
+import { getRequestMethod, setupApp, TestApp } from '@island.is/testing/nest'
+import { User } from '@island.is/auth-nest-tools'
+import { FixtureFactory } from '@island.is/services/auth/testing'
+import {
+ createCurrentUser,
+ createNationalRegistryUser,
+} from '@island.is/testing/fixtures'
+import { DelegationAdminScopes } from '@island.is/auth/scopes'
+import addDays from 'date-fns/addDays'
+import {
+ CreatePaperDelegationDto,
+ Delegation,
+ DELEGATION_TAG,
+ DelegationDelegationType,
+ DelegationsIndexService,
+ SequelizeConfigService,
+ ZENDESK_CUSTOM_FIELDS,
+} from '@island.is/auth-api-lib'
+
+import { AppModule } from '../../../app.module'
+import { AuthDelegationType } from '@island.is/shared/types'
+import { getModelToken } from '@nestjs/sequelize'
+import { faker } from '@island.is/shared/mocking'
+import { TicketStatus, ZendeskService } from '@island.is/clients/zendesk'
+import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2'
+
+const currentUser = createCurrentUser({
+ scope: [DelegationAdminScopes.read, DelegationAdminScopes.admin],
+})
+
+describe('DelegationAdmin - With authentication', () => {
+ let app: TestApp
+ let server: request.SuperTest
+ let factory: FixtureFactory
+ let zendeskService: ZendeskService
+ let nationalRegistryApi: NationalRegistryClientService
+ let delegationIndexServiceApi: DelegationsIndexService
+
+ beforeEach(async () => {
+ app = await setupApp({
+ AppModule,
+ SequelizeConfigService,
+ user: currentUser,
+ dbType: 'postgres',
+ })
+
+ server = request(app.getHttpServer())
+ factory = new FixtureFactory(app)
+
+ zendeskService = app.get(ZendeskService)
+ nationalRegistryApi = app.get(NationalRegistryClientService)
+ delegationIndexServiceApi = app.get(DelegationsIndexService)
+
+ jest
+ .spyOn(delegationIndexServiceApi, 'indexCustomDelegations')
+ .mockImplementation(async () => {
+ return
+ })
+
+ jest
+ .spyOn(delegationIndexServiceApi, 'indexGeneralMandateDelegations')
+ .mockImplementation(async () => {
+ return
+ })
+ })
+
+ afterEach(async () => {
+ await app.cleanUp()
+ })
+
+ async function createDelegationAdmin(user?: User) {
+ const domain = await factory.createDomain({
+ name: 'd1',
+ apiScopes: [
+ { name: 's1', supportedDelegationTypes: [AuthDelegationType.Custom] },
+ ],
+ })
+
+ return factory.createCustomDelegation({
+ fromNationalId: user?.nationalId ?? '',
+ domainName: domain.name,
+ scopes: [{ scopeName: 's1' }],
+ referenceId: 'ref1',
+ })
+ }
+
+ describe('GET /delegation-admin', () => {
+ it('GET /delegation-admin should return delegations for nationalId', async () => {
+ // Arrange
+ const delegation = await createDelegationAdmin(currentUser)
+ // Act
+ const res = await getRequestMethod(
+ server,
+ 'GET',
+ )('/delegation-admin').set('X-Query-National-Id', currentUser.nationalId)
+
+ // Assert
+ expect(res.status).toEqual(200)
+ expect(res.body['outgoing'][0].id).toEqual(delegation.id)
+ })
+ })
+
+ describe('DELETE /delegation-admin/:delegation', () => {
+ it('DELETE /delegation-admin/:delegation should not delete delegation that has no reference id', async () => {
+ // Arrange
+ const delegationModel = await app.get(getModelToken(Delegation))
+ const delegation = await createDelegationAdmin(currentUser)
+ // Remove the referenceId
+ await delegationModel.update(
+ {
+ referenceId: null,
+ },
+ {
+ where: {
+ id: delegation.id,
+ },
+ },
+ )
+
+ // Act
+ const res = await getRequestMethod(
+ server,
+ 'DELETE',
+ )(`/delegation-admin/${delegation.id}`)
+
+ // Assert
+ expect(res.status).toEqual(204)
+
+ // Assert db
+ const deletedDelegation = await delegationModel.findByPk(delegation.id)
+
+ expect(deletedDelegation).not.toBeNull()
+ })
+
+ it('DELETE /delegation-admin/:delegation should delete delegation', async () => {
+ // Arrange
+ const delegation = await createDelegationAdmin(currentUser)
+
+ // Act
+ const res = await getRequestMethod(
+ server,
+ 'DELETE',
+ )(`/delegation-admin/${delegation.id}`)
+
+ // Assert
+ expect(res.status).toEqual(204)
+
+ // Assert db
+ const delegationModel = await app.get(getModelToken(Delegation))
+ const deletedDelegation = await delegationModel.findByPk(delegation.id)
+
+ expect(deletedDelegation).toBeNull()
+ })
+
+ it('DELETE /delegation-admin/:delegation should throw error since id does not exist', async () => {
+ // Arrange
+ await createDelegationAdmin(currentUser)
+
+ const invalidId = faker.datatype.uuid()
+ // Act
+ const res = await getRequestMethod(
+ server,
+ 'DELETE',
+ )(`/delegation-admin/${invalidId}`)
+
+ // Assert
+ expect(res.status).toEqual(204)
+
+ // Assert db
+ const delegationModel = await app.get(getModelToken(Delegation))
+ const deletedDelegation = await delegationModel.findAll()
+
+ expect(deletedDelegation).not.toBeNull()
+ })
+ })
+
+ describe('POST /delegation-admin', () => {
+ const toNationalId = '0101302399'
+ const fromNationalId = '0101307789'
+
+ let zendeskServiceApiSpy: jest.SpyInstance
+ let nationalRegistryApiSpy: jest.SpyInstance
+
+ let delegationModel: typeof Delegation
+ let delegationDelegationTypeModel: typeof DelegationDelegationType
+
+ beforeEach(async () => {
+ delegationModel = await app.get(getModelToken(Delegation))
+ delegationDelegationTypeModel = await app.get(
+ getModelToken(DelegationDelegationType),
+ )
+
+ await factory.createDomain({
+ name: 'd1',
+ apiScopes: [
+ {
+ name: 's1',
+ supportedDelegationTypes: [
+ AuthDelegationType.Custom,
+ AuthDelegationType.GeneralMandate,
+ ],
+ },
+ ],
+ })
+
+ mockZendeskService(toNationalId, fromNationalId)
+ mockNationalRegistryService()
+ })
+
+ const mockNationalRegistryService = () => {
+ nationalRegistryApiSpy = jest
+ .spyOn(nationalRegistryApi, 'getIndividual')
+ .mockImplementation(async (id) => {
+ const user = createNationalRegistryUser({
+ nationalId: id,
+ })
+
+ return user ?? null
+ })
+ }
+
+ const mockZendeskService = (
+ toNationalId: string,
+ fromNationalId: string,
+ ) => {
+ zendeskServiceApiSpy = jest
+ .spyOn(zendeskService, 'getTicket')
+ .mockImplementation((ticketId: string) => {
+ return new Promise((resolve) =>
+ resolve({
+ id: ticketId,
+ tags: [DELEGATION_TAG],
+ status: TicketStatus.Solved,
+ custom_fields: [
+ {
+ id: ZENDESK_CUSTOM_FIELDS.DelegationToReferenceId,
+ value: toNationalId,
+ },
+ {
+ id: ZENDESK_CUSTOM_FIELDS.DelegationFromReferenceId,
+ value: fromNationalId,
+ },
+ ],
+ }),
+ )
+ })
+ }
+
+ afterEach(() => {
+ jest.restoreAllMocks()
+ })
+
+ it('POST /delegation-admin should create delegation', async () => {
+ // Arrange
+ const delegation: CreatePaperDelegationDto = {
+ toNationalId,
+ fromNationalId,
+ referenceId: 'ref1',
+ validTo: addDays(new Date(), 3),
+ }
+
+ // Act
+ const res = await getRequestMethod(
+ server,
+ 'POST',
+ )('/delegation-admin').send(delegation)
+
+ // Assert
+ expect(res.status).toEqual(201)
+ expect(res.body).toHaveProperty('id')
+ expect(res.body.fromNationalId).toEqual(fromNationalId)
+ expect(res.body.toNationalId).toEqual(toNationalId)
+ expect(res.body.referenceId).toEqual(delegation.referenceId)
+ expect(res.body.validTo).toEqual(delegation.validTo?.toISOString())
+ })
+
+ it('POST /delegation-admin should create delegation with no expiration date', async () => {
+ // Arrange
+ const delegation: CreatePaperDelegationDto = {
+ toNationalId,
+ fromNationalId,
+ referenceId: 'ref1',
+ }
+
+ // Act
+ const res = await getRequestMethod(
+ server,
+ 'POST',
+ )('/delegation-admin').send(delegation)
+
+ // Assert
+ expect(res.status).toEqual(201)
+ expect(res.body).toHaveProperty('id')
+ expect(res.body.fromNationalId).toEqual(fromNationalId)
+ expect(res.body.toNationalId).toEqual(toNationalId)
+ expect(res.body.referenceId).toEqual(delegation.referenceId)
+ expect(res.body).not.toHaveProperty('validTo')
+
+ // Assert db
+ const createdDelegation = await delegationModel.findByPk(res.body.id)
+ const createdDelegationDelegationType =
+ await delegationDelegationTypeModel.findOne({
+ where: {
+ delegationId: res.body.id,
+ },
+ })
+
+ expect(createdDelegation).not.toBeNull()
+ expect(createdDelegationDelegationType).not.toBeNull()
+ })
+
+ it('POST /delegation-admin should not create delegation with company national id', async () => {
+ // Arrange
+ const delegation: CreatePaperDelegationDto = {
+ toNationalId: '5005005001',
+ fromNationalId,
+ referenceId: 'ref1',
+ }
+
+ // Act
+ const res = await getRequestMethod(
+ server,
+ 'POST',
+ )('/delegation-admin').send(delegation)
+
+ // Assert
+ expect(res.status).toEqual(400)
+ })
+ })
+})
diff --git a/apps/services/auth/admin-api/src/openApi.ts b/apps/services/auth/admin-api/src/openApi.ts
index 7e1b27bf3538..64ae673f7545 100644
--- a/apps/services/auth/admin-api/src/openApi.ts
+++ b/apps/services/auth/admin-api/src/openApi.ts
@@ -1,8 +1,27 @@
import { DocumentBuilder } from '@nestjs/swagger'
+import { environment } from './environments'
+import { AuthScope } from '@island.is/auth/scopes'
export const openApi = new DocumentBuilder()
.setTitle('IdentityServer Admin api')
.setDescription('Api for administration.')
.setVersion('2.0')
.addTag('auth-admin-api')
+ .addOAuth2(
+ {
+ type: 'oauth2',
+ description:
+ 'Authentication and authorization using island.is authentication service (IAS).',
+ flows: {
+ authorizationCode: {
+ authorizationUrl: `${environment.auth.issuer}/connect/authorize`,
+ tokenUrl: `${environment.auth.issuer}/connect/token`,
+ scopes: {
+ openid: 'Default openid scope',
+ },
+ },
+ },
+ },
+ 'ias',
+ )
.build()
diff --git a/apps/services/auth/delegation-api/infra/delegation-api.ts b/apps/services/auth/delegation-api/infra/delegation-api.ts
index 1ceff205f6d3..60202a6822d8 100644
--- a/apps/services/auth/delegation-api/infra/delegation-api.ts
+++ b/apps/services/auth/delegation-api/infra/delegation-api.ts
@@ -1,8 +1,8 @@
import {
json,
+ ref,
service,
ServiceBuilder,
- ref,
} from '../../../../../infra/src/dsl/dsl'
import { Base, Client, RskProcuring } from '../../../../../infra/src/dsl/xroad'
@@ -54,12 +54,20 @@ export const serviceSetup = (services: {
prod: 'IS/GOV/5402696029/Skatturinn/ft-v1',
},
COMPANY_REGISTRY_REDIS_NODES: REDIS_NODE_CONFIG,
+ SYSLUMENN_HOST: {
+ dev: 'https://api.syslumenn.is/staging',
+ staging: 'https://api.syslumenn.is/staging',
+ prod: 'https://api.syslumenn.is',
+ },
+ SYSLUMENN_TIMEOUT: '3000',
})
.secrets({
IDENTITY_SERVER_CLIENT_SECRET:
'/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET',
NATIONAL_REGISTRY_IDS_CLIENT_SECRET:
'/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET',
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME',
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD',
})
.xroad(Base, Client, RskProcuring)
.readiness('/health/check')
diff --git a/apps/services/auth/delegation-api/src/app/app.module.ts b/apps/services/auth/delegation-api/src/app/app.module.ts
index 10b6209d5bce..b425506e3878 100644
--- a/apps/services/auth/delegation-api/src/app/app.module.ts
+++ b/apps/services/auth/delegation-api/src/app/app.module.ts
@@ -7,9 +7,10 @@ import {
SequelizeConfigService,
} from '@island.is/auth-api-lib'
import { AuthModule } from '@island.is/auth-nest-tools'
+import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships'
import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2'
import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry'
-import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships'
+import { SyslumennClientConfig } from '@island.is/clients/syslumenn'
import { AuditModule } from '@island.is/nest/audit'
import {
ConfigModule,
@@ -50,6 +51,7 @@ import { ScopesModule } from './scopes/scopes.module'
CompanyRegistryConfig,
XRoadConfig,
DelegationApiUserSystemNotificationConfig,
+ SyslumennClientConfig,
],
}),
],
diff --git a/apps/services/auth/ids-api/infra/ids-api.ts b/apps/services/auth/ids-api/infra/ids-api.ts
index efae3c5d56d9..e72a270d0d37 100644
--- a/apps/services/auth/ids-api/infra/ids-api.ts
+++ b/apps/services/auth/ids-api/infra/ids-api.ts
@@ -83,6 +83,12 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-ids-api'> => {
// Origin for Android prod app
'android:apk-key-hash:EsLTUu5kaY7XPmMl2f7nbq4amu-PNzdYu3FecNf90wU',
]),
+ SYSLUMENN_HOST: {
+ dev: 'https://api.syslumenn.is/staging',
+ staging: 'https://api.syslumenn.is/staging',
+ prod: 'https://api.syslumenn.is',
+ },
+ SYSLUMENN_TIMEOUT: '3000',
})
.secrets({
IDENTITY_SERVER_CLIENT_SECRET:
@@ -92,6 +98,8 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-ids-api'> => {
NOVA_PASSWORD: '/k8s/services-auth/NOVA_PASSWORD',
NATIONAL_REGISTRY_B2C_CLIENT_SECRET:
'/k8s/services-auth/NATIONAL_REGISTRY_B2C_CLIENT_SECRET',
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME',
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD',
})
.xroad(Base, Client, RskProcuring, NationalRegistryAuthB2C)
.readiness('/health/check')
diff --git a/apps/services/auth/ids-api/src/app/app.module.ts b/apps/services/auth/ids-api/src/app/app.module.ts
index 4f8a3e3d670f..f4aeb431e8c8 100644
--- a/apps/services/auth/ids-api/src/app/app.module.ts
+++ b/apps/services/auth/ids-api/src/app/app.module.ts
@@ -11,6 +11,7 @@ import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationshi
import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2'
import { NationalRegistryV3ClientConfig } from '@island.is/clients/national-registry-v3'
import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry'
+import { SyslumennClientConfig } from '@island.is/clients/syslumenn'
import { UserProfileClientConfig } from '@island.is/clients/user-profile'
import { AuditModule } from '@island.is/nest/audit'
import {
@@ -28,12 +29,12 @@ import { DelegationsModule } from './delegations/delegations.module'
import { GrantsModule } from './grants/grants.module'
import { LoginRestrictionsModule } from './login-restrictions/login-restrictions.module'
import { NotificationsModule } from './notifications/notifications.module'
+import { PasskeysModule } from './passkeys/passkeys.module'
import { PermissionsModule } from './permissions/permissions.module'
import { ResourcesModule } from './resources/resources.module'
import { TranslationModule } from './translation/translation.module'
import { UserProfileModule } from './user-profile/user-profile.module'
import { UsersModule } from './users/users.module'
-import { PasskeysModule } from './passkeys/passkeys.module'
@Module({
imports: [
@@ -68,6 +69,7 @@ import { PasskeysModule } from './passkeys/passkeys.module'
PasskeysCoreConfig,
NationalRegistryV3ClientConfig,
smsModuleConfig,
+ SyslumennClientConfig,
],
}),
],
diff --git a/apps/services/auth/ids-api/src/app/delegations/delegation-verification-result.dto.ts b/apps/services/auth/ids-api/src/app/delegations/delegation-verification-result.dto.ts
new file mode 100644
index 000000000000..6f99d970922f
--- /dev/null
+++ b/apps/services/auth/ids-api/src/app/delegations/delegation-verification-result.dto.ts
@@ -0,0 +1,8 @@
+import { ApiProperty } from '@nestjs/swagger'
+import { IsBoolean } from 'class-validator'
+
+export class DelegationVerificationResult {
+ @IsBoolean()
+ @ApiProperty()
+ verified!: boolean
+}
diff --git a/apps/services/auth/ids-api/src/app/delegations/delegation-verification.dto.ts b/apps/services/auth/ids-api/src/app/delegations/delegation-verification.dto.ts
new file mode 100644
index 000000000000..aa799809529d
--- /dev/null
+++ b/apps/services/auth/ids-api/src/app/delegations/delegation-verification.dto.ts
@@ -0,0 +1,19 @@
+import { ApiProperty } from '@nestjs/swagger'
+import { IsArray, IsEnum, IsString } from 'class-validator'
+
+import { AuthDelegationType } from '@island.is/shared/types'
+
+export class DelegationVerification {
+ @IsString()
+ @ApiProperty()
+ fromNationalId!: string
+
+ @IsArray()
+ @IsEnum(AuthDelegationType, { each: true })
+ @ApiProperty({
+ enum: AuthDelegationType,
+ enumName: 'AuthDelegationType',
+ isArray: true,
+ })
+ delegationTypes!: AuthDelegationType[]
+}
diff --git a/apps/services/auth/ids-api/src/app/delegations/delegations.controller.ts b/apps/services/auth/ids-api/src/app/delegations/delegations.controller.ts
index df91500afe6e..8310d931d823 100644
--- a/apps/services/auth/ids-api/src/app/delegations/delegations.controller.ts
+++ b/apps/services/auth/ids-api/src/app/delegations/delegations.controller.ts
@@ -1,8 +1,10 @@
import {
+ Body,
Controller,
Get,
Inject,
ParseArrayPipe,
+ Post,
Query,
UseGuards,
Version,
@@ -25,11 +27,14 @@ import {
ScopesGuard,
} from '@island.is/auth-nest-tools'
import { LOGGER_PROVIDER } from '@island.is/logging'
+import { Documentation } from '@island.is/nest/swagger'
import { AuthDelegationType } from '@island.is/shared/types'
+import { DelegationVerificationResult } from './delegation-verification-result.dto'
+import { DelegationVerification } from './delegation-verification.dto'
+
import type { Logger } from '@island.is/logging'
import type { User } from '@island.is/auth-nest-tools'
-
@UseGuards(IdsUserGuard, ScopesGuard)
@ApiTags('delegations')
@Controller({
@@ -110,4 +115,26 @@ export class DelegationsController {
delegationType,
)
}
+
+ @Scopes('@identityserver.api/authentication')
+ @Post('verify')
+ @Documentation({
+ description: 'Verifies a delegation at the source.',
+ response: { status: 200, type: DelegationVerificationResult },
+ })
+ @ApiOkResponse({ type: DelegationVerificationResult })
+ async verify(
+ @CurrentUser() user: User,
+ @Body()
+ request: DelegationVerification,
+ ): Promise {
+ const verified =
+ await this.delegationsIncomingService.verifyDelegationAtProvider(
+ user,
+ request.fromNationalId,
+ request.delegationTypes,
+ )
+
+ return { verified }
+ }
}
diff --git a/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters.spec.ts b/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters.spec.ts
index c6032e1def00..526bf4262a4a 100644
--- a/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters.spec.ts
+++ b/apps/services/auth/ids-api/src/app/delegations/test/delegations-filters.spec.ts
@@ -15,7 +15,10 @@ import {
import { createNationalRegistryUser } from '@island.is/testing/fixtures'
import { TestApp, truncate } from '@island.is/testing/nest'
-import { setupWithAuth } from '../../../../test/setup'
+import {
+ nonExistingLegalRepresentativeNationalId,
+ setupWithAuth,
+} from '../../../../test/setup'
import { testCases } from './delegations-filters-test-cases'
import { user } from './delegations-filters-types'
@@ -128,4 +131,58 @@ describe('DelegationsController', () => {
})
},
)
+
+ describe('verify', () => {
+ const testCase = testCases['legalRepresentative1']
+ testCase.user = user
+ const path = '/v1/delegations/verify'
+
+ beforeAll(async () => {
+ await truncate(sequelize)
+
+ await Promise.all(
+ testCase.domains.map((domain) => factory.createDomain(domain)),
+ )
+
+ await factory.createClient(testCase.client)
+
+ await Promise.all(
+ testCase.clientAllowedScopes.map((scope) =>
+ factory.createClientAllowedScope(scope),
+ ),
+ )
+
+ await Promise.all(
+ testCase.apiScopes.map((scope) => factory.createApiScope(scope)),
+ )
+
+ await factory.createDelegationIndexRecord({
+ fromNationalId: nonExistingLegalRepresentativeNationalId,
+ toNationalId: testCase.user.nationalId,
+ type: AuthDelegationType.LegalRepresentative,
+ provider: AuthDelegationProvider.DistrictCommissionersRegistry,
+ })
+ })
+
+ let res: request.Response
+ it(`POST ${path} returns verified response`, async () => {
+ res = await server.post(path).send({
+ fromNationalId: testCase.fromLegalRepresentative[0],
+ delegationTypes: [AuthDelegationType.LegalRepresentative],
+ })
+
+ expect(res.status).toEqual(200)
+ expect(res.body.verified).toEqual(true)
+ })
+
+ it(`POST ${path} returns non-verified response`, async () => {
+ res = await server.post(path).send({
+ fromNationalId: nonExistingLegalRepresentativeNationalId,
+ delegationTypes: [AuthDelegationType.LegalRepresentative],
+ })
+
+ expect(res.status).toEqual(200)
+ expect(res.body.verified).toEqual(false)
+ })
+ })
})
diff --git a/apps/services/auth/ids-api/src/app/delegations/test/delegations-scopes.spec.ts b/apps/services/auth/ids-api/src/app/delegations/test/delegations-scopes.spec.ts
index 31915631e076..61367667d5c2 100644
--- a/apps/services/auth/ids-api/src/app/delegations/test/delegations-scopes.spec.ts
+++ b/apps/services/auth/ids-api/src/app/delegations/test/delegations-scopes.spec.ts
@@ -22,12 +22,14 @@ const legalGuardianScopes = ['lg1', 'lg2']
const procurationHolderScopes = ['ph1', 'ph2']
const customScopes1 = ['cu1', 'cu2']
const customScopes2 = ['cu3', 'cu4']
+const legalRepresentativeScopes = ['lr1', 'lr2']
const apiScopes = [
...legalGuardianScopes,
...procurationHolderScopes,
...customScopes1,
...customScopes2,
+ ...legalRepresentativeScopes,
]
const fromCustom = [
@@ -48,6 +50,9 @@ const supportedDelegationTypes = (scopeName: string): AuthDelegationType[] => {
if (customScopes1.includes(scopeName) || customScopes2.includes(scopeName)) {
result.push(AuthDelegationType.Custom)
}
+ if (legalRepresentativeScopes.includes(scopeName)) {
+ result.push(AuthDelegationType.LegalRepresentative)
+ }
return result
}
@@ -98,6 +103,11 @@ const testCases: Record = {
],
expected: [...legalGuardianScopes, ...identityResources],
},
+ '7': {
+ fromNationalId: createNationalId('person'),
+ delegationType: [AuthDelegationType.LegalRepresentative],
+ expected: [...legalRepresentativeScopes, ...identityResources],
+ },
}
const user = createCurrentUser({
diff --git a/apps/services/auth/ids-api/test/setup.ts b/apps/services/auth/ids-api/test/setup.ts
index c6b7ae7f9e7b..a04e722bfcc8 100644
--- a/apps/services/auth/ids-api/test/setup.ts
+++ b/apps/services/auth/ids-api/test/setup.ts
@@ -12,6 +12,7 @@ import { RskRelationshipsClient } from '@island.is/clients-rsk-relationships'
import { NationalRegistryClientService } from '@island.is/clients/national-registry-v2'
import { NationalRegistryV3ClientService } from '@island.is/clients/national-registry-v3'
import { CompanyRegistryClientService } from '@island.is/clients/rsk/company-registry'
+import { SyslumennService } from '@island.is/clients/syslumenn'
import { V2MeApi } from '@island.is/clients/user-profile'
import { FeatureFlagService, Features } from '@island.is/nest/feature-flags'
import {
@@ -21,6 +22,7 @@ import {
} from '@island.is/services/auth/testing'
import {
createCurrentUser,
+ createNationalId,
createUniqueWords,
} from '@island.is/testing/fixtures'
import {
@@ -67,6 +69,8 @@ export const defaultScopes: Scopes = {
},
}
+export const nonExistingLegalRepresentativeNationalId = createNationalId()
+
class MockNationalRegistryClientService
implements Partial
{
@@ -85,6 +89,13 @@ class MockUserProfile {
meUserProfileControllerFindUserProfile = jest.fn().mockResolvedValue({})
}
+class MockSyslumennService {
+ checkIfDelegationExists = jest.fn(
+ (_toNationalId: string, fromNationalId: string) =>
+ fromNationalId !== nonExistingLegalRepresentativeNationalId,
+ )
+}
+
interface SetupOptions {
user: User
scopes?: Scopes
@@ -125,6 +136,8 @@ export const setupWithAuth = async ({
.useValue({
getIndividualRelationships: jest.fn().mockResolvedValue(null),
})
+ .overrideProvider(SyslumennService)
+ .useClass(MockSyslumennService)
.overrideProvider(FeatureFlagService)
.useValue({
getValue: (feature: Features) =>
diff --git a/apps/services/auth/personal-representative/infra/personal-representative.ts b/apps/services/auth/personal-representative/infra/personal-representative.ts
index 9a54b8c4206b..bb3c79004506 100644
--- a/apps/services/auth/personal-representative/infra/personal-representative.ts
+++ b/apps/services/auth/personal-representative/infra/personal-representative.ts
@@ -42,10 +42,18 @@ export const serviceSetup =
prod: 'IS/GOV/5402696029/Skatturinn/ft-v1',
},
COMPANY_REGISTRY_REDIS_NODES: REDIS_NODE_CONFIG,
+ SYSLUMENN_HOST: {
+ dev: 'https://api.syslumenn.is/staging',
+ staging: 'https://api.syslumenn.is/staging',
+ prod: 'https://api.syslumenn.is',
+ },
+ SYSLUMENN_TIMEOUT: '3000',
})
.secrets({
IDENTITY_SERVER_CLIENT_SECRET:
'/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET',
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME',
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD',
})
.xroad(Base, Client, RskProcuring)
.ingress({
diff --git a/apps/services/auth/personal-representative/src/app/app.module.ts b/apps/services/auth/personal-representative/src/app/app.module.ts
index 14a44d76dc13..74d0e3f0c759 100644
--- a/apps/services/auth/personal-representative/src/app/app.module.ts
+++ b/apps/services/auth/personal-representative/src/app/app.module.ts
@@ -1,26 +1,29 @@
-import { RightTypesModule } from './modules/rightTypes/rightTypes.module'
-import { PersonalRepresentativesModule } from './modules/personalRepresentatives/personalRepresentatives.module'
-import { PersonalRepresentativeTypesModule } from './modules/personalRepresentativeTypes/personalRepresentativeTypes.module'
-import { AccessLogsModule } from './modules/accessLogs/accessLogs.module'
+import { Module } from '@nestjs/common'
+import { SequelizeModule } from '@nestjs/sequelize'
+
import {
DelegationConfig,
SequelizeConfigService,
} from '@island.is/auth-api-lib'
-import { Module } from '@nestjs/common'
-import { SequelizeModule } from '@nestjs/sequelize'
-import { environment } from '../environments'
-import { AuditModule } from '@island.is/nest/audit'
import { AuthModule } from '@island.is/auth-nest-tools'
+import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships'
+import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2'
+import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry'
+import { SyslumennClientConfig } from '@island.is/clients/syslumenn'
+import { AuditModule } from '@island.is/nest/audit'
import {
ConfigModule,
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'
-import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships'
import { FeatureFlagConfig } from '@island.is/nest/feature-flags'
+import { environment } from '../environments'
+import { AccessLogsModule } from './modules/accessLogs/accessLogs.module'
+import { PersonalRepresentativesModule } from './modules/personalRepresentatives/personalRepresentatives.module'
+import { PersonalRepresentativeTypesModule } from './modules/personalRepresentativeTypes/personalRepresentativeTypes.module'
+import { RightTypesModule } from './modules/rightTypes/rightTypes.module'
+
@Module({
imports: [
AuditModule.forRoot(environment.audit),
@@ -38,6 +41,7 @@ import { FeatureFlagConfig } from '@island.is/nest/feature-flags'
CompanyRegistryConfig,
XRoadConfig,
FeatureFlagConfig,
+ SyslumennClientConfig,
],
}),
RightTypesModule,
diff --git a/apps/services/auth/public-api/infra/auth-public-api.ts b/apps/services/auth/public-api/infra/auth-public-api.ts
index 68c82c383cf0..28e81a88467a 100644
--- a/apps/services/auth/public-api/infra/auth-public-api.ts
+++ b/apps/services/auth/public-api/infra/auth-public-api.ts
@@ -1,5 +1,4 @@
-import { service, ServiceBuilder } from '../../../../../infra/src/dsl/dsl'
-import { json } from '../../../../../infra/src/dsl/dsl'
+import { json, service, ServiceBuilder } from '../../../../../infra/src/dsl/dsl'
import { Base, Client, RskProcuring } from '../../../../../infra/src/dsl/xroad'
const REDIS_NODE_CONFIG = {
@@ -64,12 +63,20 @@ export const serviceSetup = (): ServiceBuilder<'services-auth-public-api'> => {
// Origin for Android prod app
'android:apk-key-hash:EsLTUu5kaY7XPmMl2f7nbq4amu-PNzdYu3FecNf90wU',
]),
+ SYSLUMENN_HOST: {
+ dev: 'https://api.syslumenn.is/staging',
+ staging: 'https://api.syslumenn.is/staging',
+ prod: 'https://api.syslumenn.is',
+ },
+ SYSLUMENN_TIMEOUT: '3000',
})
.secrets({
IDENTITY_SERVER_CLIENT_SECRET:
'/k8s/services-auth/IDENTITY_SERVER_CLIENT_SECRET',
NATIONAL_REGISTRY_IDS_CLIENT_SECRET:
'/k8s/xroad/client/NATIONAL-REGISTRY/IDENTITYSERVER_SECRET',
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME',
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD',
})
.xroad(Base, Client, RskProcuring)
.ingress({
diff --git a/apps/services/auth/public-api/src/app/app.module.ts b/apps/services/auth/public-api/src/app/app.module.ts
index 299a9219a419..a6863c79a866 100644
--- a/apps/services/auth/public-api/src/app/app.module.ts
+++ b/apps/services/auth/public-api/src/app/app.module.ts
@@ -2,11 +2,15 @@ import { Module } from '@nestjs/common'
import { SequelizeModule } from '@nestjs/sequelize'
import {
- SequelizeConfigService,
DelegationConfig,
PasskeysCoreConfig,
+ SequelizeConfigService,
} from '@island.is/auth-api-lib'
import { AuthModule } from '@island.is/auth-nest-tools'
+import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships'
+import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2'
+import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry'
+import { SyslumennClientConfig } from '@island.is/clients/syslumenn'
import { AuditModule } from '@island.is/nest/audit'
import {
ConfigModule,
@@ -15,9 +19,6 @@ import {
} from '@island.is/nest/config'
import { FeatureFlagConfig } from '@island.is/nest/feature-flags'
import { ProblemModule } from '@island.is/nest/problem'
-import { NationalRegistryClientConfig } from '@island.is/clients/national-registry-v2'
-import { CompanyRegistryConfig } from '@island.is/clients/rsk/company-registry'
-import { RskRelationshipsClientConfig } from '@island.is/clients-rsk-relationships'
import { environment } from '../environments'
import { DelegationsModule } from './modules/delegations/delegations.module'
@@ -44,6 +45,7 @@ import { PasskeysModule } from './modules/passkeys/passkeys.module'
CompanyRegistryConfig,
XRoadConfig,
PasskeysCoreConfig,
+ SyslumennClientConfig,
],
}),
],
diff --git a/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx b/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx
index 53333b718288..7c4cda33f594 100644
--- a/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx
+++ b/apps/skilavottord/web/screens/DeregisterVehicle/Confirm/Confirm.tsx
@@ -101,6 +101,10 @@ const UpdateSkilavottordVehicleInfoMutation = gql`
const Confirm: FC> = () => {
const [reloadFlag, setReloadFlag] = useState(false)
+ const [
+ vehicleReadyToDeregisteredQueryCompleted,
+ setVehicleReadyToDeregisteredQueryCompleted,
+ ] = useState(false)
// Update reloadFlag to trigger the child component to reload
const triggerReload = () => {
@@ -134,6 +138,11 @@ const Confirm: FC> = () => {
SkilavottordVehicleReadyToDeregisteredQuery,
{
variables: { permno: id },
+ onCompleted: (data) => {
+ if (data && data.skilavottordVehicleReadyToDeregistered) {
+ setVehicleReadyToDeregisteredQueryCompleted(true)
+ }
+ },
},
)
@@ -143,6 +152,7 @@ const Confirm: FC> = () => {
SkilavottordTrafficQuery,
{
variables: { permno: id },
+ skip: !vehicleReadyToDeregisteredQueryCompleted,
},
)
diff --git a/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanies.tsx b/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanies.tsx
index de4040c67755..89f0aee3bd0f 100644
--- a/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanies.tsx
+++ b/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanies.tsx
@@ -72,7 +72,7 @@ const RecyclingCompanies: FC> = () => {
const handleUpdate = (id: string) => {
router.push({
- pathname: BASE_PATH + routes.recyclingCompanies.edit, // without BASE-PATH it changes the whole route, probably some bug
+ pathname: routes.recyclingCompanies.edit, // with BASE-PATH it duplicates the path
query: { id },
})
}
diff --git a/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanyUpdate/RecyclingCompanyUpdate.tsx b/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanyUpdate/RecyclingCompanyUpdate.tsx
index aa2d7ce91e9b..4d1357720c0f 100644
--- a/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanyUpdate/RecyclingCompanyUpdate.tsx
+++ b/apps/skilavottord/web/screens/RecyclingCompanies/RecyclingCompanyUpdate/RecyclingCompanyUpdate.tsx
@@ -115,6 +115,9 @@ const RecyclingCompanyUpdate: FC> = () => {
}
const handleUpdateRecyclingPartner = handleSubmit(async (input) => {
+ // Not needed to be sent to the backend, causes error if it is sent
+ delete input.__typename
+
const { errors } = await updateSkilavottordRecyclingPartner({
variables: { input },
})
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 58bd1fc96afd..caac3a135fda 100644
--- a/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.service.ts
+++ b/apps/skilavottord/ws/src/app/modules/samgongustofa/samgongustofa.service.ts
@@ -347,23 +347,49 @@ export class SamgongustofaService {
)
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,
- },
+ if (result.data.length) {
+ // 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)}`,
+ {
+ permno: getShortPermno(traffic.permno),
+ outInStatus: traffic.outInStatus,
+ useStatus: traffic.useStatus,
+ useStatusName: traffic.useStatusName,
+ },
+ )
+
+ //
+ if (!traffic.outInStatus) {
+ logger.warn(
+ `car-recycling: No traffic data being returned for ${getShortPermno(
+ permno,
+ )}`,
+ { dataFromServer: result.data },
+ )
+ }
+
+ return traffic
+ }
+
+ logger.warn(
+ `car-recycling: No traffic data found for ${getShortPermno(permno)}`,
)
- return traffic
+ return {
+ permno,
+ outInStatus: '',
+ useStatus: '',
+ useStatusName: '',
+ } as Traffic
}
throw new Error(
diff --git a/apps/system-e2e/src/tests/judicial-system/regression/custody-tests.spec.ts b/apps/system-e2e/src/tests/judicial-system/regression/custody-tests.spec.ts
index ab73431c37a9..a5974aef207c 100644
--- a/apps/system-e2e/src/tests/judicial-system/regression/custody-tests.spec.ts
+++ b/apps/system-e2e/src/tests/judicial-system/regression/custody-tests.spec.ts
@@ -63,7 +63,7 @@ test.describe.serial('Custody tests', () => {
.fill(randomPoliceCaseNumber())
await page.getByRole('button', { name: 'Skrá númer' }).click()
await page.getByRole('checkbox').first().check()
- await page.locator('input[name=accusedName]').fill(faker.name.findName())
+ await page.locator('input[name=inputName]').fill(faker.name.findName())
await page.locator('input[name=accusedAddress]').fill('Einhversstaðar 1')
await page.locator('#defendantGender').click()
await page.locator('#react-select-defendantGender-option-0').click()
diff --git a/apps/system-e2e/src/tests/judicial-system/regression/indictment-tests.spec.ts b/apps/system-e2e/src/tests/judicial-system/regression/indictment-tests.spec.ts
index 032baf8fb428..b8b723faa91c 100644
--- a/apps/system-e2e/src/tests/judicial-system/regression/indictment-tests.spec.ts
+++ b/apps/system-e2e/src/tests/judicial-system/regression/indictment-tests.spec.ts
@@ -39,11 +39,11 @@ test.describe.serial('Indictment tests', () => {
await page
.getByRole('checkbox', { name: 'Ákærði er ekki með íslenska kennitölu' })
.check()
- await page.getByTestId('nationalId').click()
- await page.getByTestId('nationalId').fill('01.01.2000')
- await page.getByTestId('accusedName').click()
- await page.getByTestId('accusedName').fill(accusedName)
- await page.getByTestId('accusedName').press('Tab')
+ await page.getByTestId('inputNationalId').click()
+ await page.getByTestId('inputNationalId').fill('01.01.2000')
+ await page.getByTestId('inputName').click()
+ await page.getByTestId('inputName').fill(accusedName)
+ await page.getByTestId('inputName').press('Tab')
await page.getByTestId('accusedAddress').fill('Testgata 12')
await page.locator('#defendantGender').click()
await page.locator('#react-select-defendantGender-option-0').click()
@@ -78,10 +78,15 @@ test.describe.serial('Indictment tests', () => {
page.getByText('Játar sök').click(),
verifyRequestCompletion(page, '/api/graphql', 'UpdateDefendant'),
])
+ await Promise.all([
+ page.getByText('Nei').last().click(),
+ verifyRequestCompletion(page, '/api/graphql', 'UpdateCase'),
+ ])
await Promise.all([
page.getByTestId('continueButton').click(),
verifyRequestCompletion(page, '/api/graphql', 'Case'),
])
+
// Case files
await expect(page).toHaveURL(`/akaera/domskjol/${caseId}`)
diff --git a/apps/system-e2e/src/tests/judicial-system/regression/search-warrant-tests.spec.ts b/apps/system-e2e/src/tests/judicial-system/regression/search-warrant-tests.spec.ts
index 40fd809eac8f..64e139713d95 100644
--- a/apps/system-e2e/src/tests/judicial-system/regression/search-warrant-tests.spec.ts
+++ b/apps/system-e2e/src/tests/judicial-system/regression/search-warrant-tests.spec.ts
@@ -41,7 +41,7 @@ test.describe.serial('Search warrant tests', () => {
await page.locator('#type').click()
await page.locator('#react-select-type-option-0').click()
await page.getByRole('checkbox').first().check()
- await page.locator('input[name=accusedName]').fill(faker.name.findName())
+ await page.locator('input[name=inputName]').fill(faker.name.findName())
await page.locator('input[name=accusedAddress]').fill('Einhversstaðar 1')
await page.locator('#defendantGender').click()
await page.locator('#react-select-defendantGender-option-0').click()
diff --git a/apps/web/components/Charts/v2/utils/format.ts b/apps/web/components/Charts/v2/utils/format.ts
index f324f599f1df..275429276ed2 100644
--- a/apps/web/components/Charts/v2/utils/format.ts
+++ b/apps/web/components/Charts/v2/utils/format.ts
@@ -43,12 +43,12 @@ export const formatValueForPresentation = (
let divider = 1
let postfix = ''
- let precision = 0
+ let precision = increasePrecisionBy
if (reduceAndRoundValue && value >= 1e6) {
divider = 1e6
postfix = messages[activeLocale].millionPostfix
- precision = 1 + increasePrecisionBy
+ precision += 1
} else if (reduceAndRoundValue && value >= 1e4) {
divider = 1e3
postfix = messages[activeLocale].thousandPostfix
diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts b/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts
index 5247afc5c39c..5ccd9797f6b2 100644
--- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts
+++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.css.ts
@@ -104,3 +104,45 @@ export const rikissaksoknariHeaderGridContainerWidth = style([
export const rikissaksoknariHeaderGridContainerSubpage =
rikissaksoknariHeaderGridContainerBase
+
+export const rikislogmadurHeaderGridContainerWidthBase = style({
+ display: 'grid',
+ maxWidth: '1342px',
+ margin: '0 auto',
+ backgroundBlendMode: 'saturation',
+ backgroundRepeat: 'no-repeat',
+ background:
+ 'linear-gradient(178.67deg, rgba(0, 61, 133, 0.2) 1.87%, rgba(0, 61, 133, 0.3) 99.6%)',
+ ...themeUtils.responsiveStyle({
+ lg: {
+ gridTemplateRows: '315px',
+ gridTemplateColumns: '60fr 40fr',
+ },
+ }),
+})
+
+export const rikislogmadurHeaderGridContainerWidth = style([
+ rikislogmadurHeaderGridContainerWidthBase,
+ themeUtils.responsiveStyle({
+ lg: {
+ background: `linear-gradient(178.67deg, rgba(0, 61, 133, 0.2) 1.87%, rgba(0, 61, 133, 0.3) 99.6%),
+ url('https://images.ctfassets.net/8k0h54kbe6bj/40IgMzNknBQUINDZZwblR/6c7dfdcf0acb3612f2bf61d912c3dd46/rikislogmadur-header-image.png') no-repeat right`,
+ },
+ }),
+])
+
+export const rikislogmadurHeaderGridContainerWidthSubpage =
+ rikislogmadurHeaderGridContainerWidthBase
+
+export const hveHeaderGridContainer = style({
+ display: 'grid',
+ maxWidth: '1342px',
+ margin: '0 auto',
+ borderBottom: '8px solid #F01E28',
+ ...themeUtils.responsiveStyle({
+ lg: {
+ gridTemplateRows: '315px',
+ gridTemplateColumns: '65fr 35fr',
+ },
+ }),
+})
diff --git a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx
index bd017feeb446..af00b7732f18 100644
--- a/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx
+++ b/apps/web/components/Organization/Wrapper/OrganizationWrapper.tsx
@@ -377,7 +377,16 @@ export const OrganizationHeader: React.FC<
/>
)
case 'rikislogmadur':
- return (
+ return n('usingDefaultHeader', false) ? (
+
+ ) : (
)
case 'hve':
- return (
+ return n('usingDefaultHeader', false) ? (
+
+ ) : (
)
case 'rettindagaesla-fatlads-folks':
- return (
+ return n('usingDefaultHeader', false) ? (
+
+ ) : (
) => {
+ return (
+
+
+ {heading}
+
+ {description && {description}}
+ {children}
+
+ )
+}
+
+interface ResultCardProps {
+ title: string
+ icon?: ReactNode
+ description: string
+}
+
+const ResultCard = ({ title, icon, description }: ResultCardProps) => {
+ return (
+
+
+
+ {icon && icon}
+ {title}
+
+
+ {description}
+
+
+ )
+}
+
+const canCalculate = (current: UserInput, previous: UserInput | null) => {
+ if (
+ !(
+ current.nameOfProcess.length > 0 &&
+ current.amountPerYear > 0 &&
+ current.processDurationInMinutes > 0 &&
+ current.visitCountToCompleteProcess > 0 &&
+ current.averageDistanceToProcessInKilometers > 0
+ )
+ ) {
+ return false
+ }
+
+ if (!previous) {
+ return true
+ }
+
+ for (const key in current) {
+ if (
+ current[key as keyof typeof current] !==
+ previous[key as keyof typeof current]
+ ) {
+ return true
+ }
+ }
+
+ return false
+}
+
+interface BenefitsOfDigitalProcessesCalculatorProps {
+ slice: ConnectedComponent
+}
+
+export const BenefitsOfDigitalProcessesCalculator = ({
+ slice,
+}: BenefitsOfDigitalProcessesCalculatorProps) => {
+ const { formatMessage } = useIntl()
+ const { activeLocale } = useI18n()
+
+ const resultsRef = useRef(null)
+ const [previousInput, setPreviousInput] = useState(null)
+
+ const [userInput, setUserInput] = useState({
+ amountPerYear: 0,
+ averageDistanceToProcessInKilometers:
+ slice.configJson?.['defaultAverageDistanceToProcessInKilometers'] ?? 7.5,
+ nameOfProcess: '',
+ processDurationInMinutes: 0,
+ visitCountToCompleteProcess: 0,
+ })
+
+ const resultColumnSpan: SpanType = ['1/1', '1/2', '1/1', '1/2', '1/3']
+
+ const { results, gainPerCitizen, ringRoadTripsSaved, co2 } = calculateResults(
+ slice,
+ userInput,
+ )
+
+ const displayResults =
+ Boolean(previousInput) &&
+ !canCalculate(userInput, previousInput) &&
+ userInput.nameOfProcess.length > 0 &&
+ userInput.amountPerYear > 0 &&
+ userInput.processDurationInMinutes > 0 &&
+ userInput.visitCountToCompleteProcess > 0 &&
+ userInput.averageDistanceToProcessInKilometers > 0
+
+ useEffect(() => {
+ if (!previousInput) return
+ resultsRef.current?.scrollIntoView({
+ behavior: 'smooth',
+ })
+ }, [previousInput])
+
+ return (
+
+
+
+
+ {
+ setUserInput((prevInput) => ({
+ ...prevInput,
+ nameOfProcess: ev.target.value,
+ }))
+ }}
+ label={formatMessage(t.nameOfProcess.label)}
+ placeholder={formatMessage(t.nameOfProcess.placeholder)}
+ />
+
+
+
+ {
+ setUserInput((prevInput) => ({
+ ...prevInput,
+ amountPerYear: Number(value),
+ }))
+ }}
+ customInput={Input}
+ name="amountPerYear"
+ id="amountPerYear"
+ type="text"
+ inputMode="numeric"
+ thousandSeparator="."
+ decimalSeparator=","
+ label={formatMessage(t.amountPerYear.label)}
+ placeholder={formatMessage(t.amountPerYear.placeholder)}
+ />
+
+
+
+ {
+ setUserInput((prevInput) => ({
+ ...prevInput,
+ processDurationInMinutes: Number(value),
+ }))
+ }}
+ customInput={Input}
+ name="processDurationInMinutes"
+ id="processDurationInMinutes"
+ type="text"
+ inputMode="numeric"
+ thousandSeparator="."
+ decimalSeparator=","
+ label={formatMessage(t.processDurationInMinutes.label)}
+ placeholder={formatMessage(
+ t.processDurationInMinutes.placeholder,
+ )}
+ />
+
+
+
+ {
+ setUserInput((prevInput) => ({
+ ...prevInput,
+ visitCountToCompleteProcess: Number(value),
+ }))
+ }}
+ customInput={Input}
+ name="visitCountToCompleteProcess"
+ id="visitCountToCompleteProcess"
+ type="text"
+ inputMode="numeric"
+ thousandSeparator="."
+ decimalSeparator=","
+ label={formatMessage(t.visitCountToCompleteProcess.label)}
+ placeholder={formatMessage(
+ t.visitCountToCompleteProcess.placeholder,
+ )}
+ />
+
+
+
+ {
+ setUserInput((prevInput) => ({
+ ...prevInput,
+ averageDistanceToProcessInKilometers: Number(value),
+ }))
+ }}
+ isNumericString={true}
+ customInput={Input}
+ name="averageDistanceToProcessInKilometers"
+ id="averageDistanceToProcessInKilometers"
+ inputMode="decimal"
+ thousandSeparator="."
+ decimalSeparator=","
+ label={formatMessage(
+ t.averageDistanceToProcessInKilometers.label,
+ )}
+ />
+
+
+
+
+
+
+
+ {displayResults && (
+
+ {userInput.nameOfProcess}
+
+
+
+ = 1e6
+ ? `${formatValueForPresentation(
+ activeLocale,
+ results.institutionGain,
+ )}${formatMessage(t.results.currencyPostfix)}`
+ : (formatCurrency(
+ results.institutionGain,
+ formatMessage(t.results.currencyPostfix),
+ ) as string)
+ }
+ description={formatMessage(
+ t.results.institutionGainDescription,
+ )}
+ icon={}
+ />
+
+
+ }
+ />
+
+
+ = 1e6
+ ? `${formatValueForPresentation(
+ activeLocale,
+ gainPerCitizen,
+ )}${formatMessage(t.results.currencyPostfix)}`
+ : (formatCurrency(
+ gainPerCitizen,
+ formatMessage(t.results.currencyPostfix),
+ ) as string)
+ }
+ description={formatMessage(t.results.citizenGainDescription, {
+ nameOfProcess: userInput.nameOfProcess,
+ })}
+ icon={}
+ />
+
+
+ }
+ />
+
+
+ }
+ />
+
+
+ = 1e6
+ ? `${formatValueForPresentation(
+ activeLocale,
+ co2,
+ )}${formatMessage(t.results.kgPostfix)}`
+ : (formatCurrency(
+ co2,
+ formatMessage(t.results.kgPostfix),
+ ) as string)
+ }
+ description={formatMessage(t.results.c02)}
+ icon={}
+ />
+
+
+
+ )}
+
+
+ )
+}
diff --git a/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/translation.strings.ts b/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/translation.strings.ts
new file mode 100644
index 000000000000..553e4111e9ff
--- /dev/null
+++ b/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/translation.strings.ts
@@ -0,0 +1,167 @@
+import { defineMessages } from 'react-intl'
+
+export const t = {
+ nameOfProcess: defineMessages({
+ heading: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:nameOfProcess.heading',
+ defaultMessage: 'Nafn ferils',
+ description: 'Heading á "nafn ferils" reit',
+ },
+ label: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:nameOfProcess.label',
+ defaultMessage: 'Nafn ferils',
+ description: 'Label á "nafn ferils" reit',
+ },
+ placeholder: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:nameOfProcess.placeholder',
+ defaultMessage: ' ',
+ description: 'Placeholder á "nafn ferils" reit',
+ },
+ }),
+ amountPerYear: defineMessages({
+ heading: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:amountPerYear.heading',
+ defaultMessage: 'Magn á ári',
+ description: 'Heading á "Fjöldi afgreiðslna á ári" reit',
+ },
+ label: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:amountPerYear.label',
+ defaultMessage: 'Fjöldi afgreiðslna á ári',
+ description: 'Label á "magn á ári" reit',
+ },
+ placeholder: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:amountPerYear.placeholder',
+ defaultMessage: ' ',
+ description: 'Placeholder á "magn á ári" reit',
+ },
+ description: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:amountPerYear.description',
+ defaultMessage: 'Fjöldi afgreiðslna á ákveðinni þjónustu á einu ári',
+ description: 'Lýsing á "magn á ári" reit',
+ },
+ }),
+ processDurationInMinutes: defineMessages({
+ heading: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:processDurationInMinutes.heading',
+ defaultMessage: 'Lengd afgreiðslu í mínútum',
+ description: 'Heading á "Lengd afgreiðslu í mínútum" reit',
+ },
+ label: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:processDurationInMinutes.label',
+ defaultMessage: 'Lengd afgreiðslu í mínútum',
+ description: 'Label á "Lengd afgreiðslu í mínútum" reit',
+ },
+ placeholder: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:processDurationInMinutes.placeholder',
+ defaultMessage: ' ',
+ description: 'Placeholder á "Lengd afgreiðslu í mínútum"',
+ },
+ description: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:processDurationInMinutes.description',
+ defaultMessage:
+ 'Áætluð lengd afgreiðslu. Biðtími þjónustuþega er ekki meðtalinn.',
+ description: 'Lýsing á "Lengd afgreiðslu í mínútum"',
+ },
+ }),
+ visitCountToCompleteProcess: defineMessages({
+ heading: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:visitCountToCompleteProcess.heading',
+ defaultMessage: 'Fjöldi heimsókna',
+ description: 'Heading á "Fjöldi heimsókna" reit',
+ },
+ label: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:visitCountToCompleteProcess.label',
+ defaultMessage: 'Fjöldi heimsókna',
+ description: 'Label á "Fjöldi heimsókna" reit',
+ },
+ placeholder: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:visitCountToCompleteProcess.placeholder',
+ defaultMessage: ' ',
+ description: 'Placeholder á "Fjöldi heimsókna" reit',
+ },
+ description: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:visitCountToCompleteProcess.description',
+ defaultMessage:
+ 'Fjöldi heimsókna sem þarf til að ljúka afgreiðslu. Ef það þarf að mæta á staðinn til þess að sækja um og koma svo aftur til þess að sækja t.d. vottorð skal slá inn 2.',
+ description: 'Lýsing á "Fjöldi heimsókna" reit',
+ },
+ }),
+ averageDistanceToProcessInKilometers: defineMessages({
+ heading: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:averageDistanceToProcessInKilometers.heading',
+ defaultMessage: 'Lengd ferðar í kílómetrum',
+ description: 'Heading á "Lengd ferðar í kílómetrum" reit',
+ },
+ label: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:averageDistanceToProcessInKilometers.label',
+ defaultMessage: 'Lengd ferðar í kílómetrum',
+ description: 'Label á "Lengd ferðar í kílómetrum" reit',
+ },
+ placeholder: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:averageDistanceToProcessInKilometers.placeholder',
+ defaultMessage: ' ',
+ description: 'Placeholder á "Lengd ferðar í kílómetrum" reit',
+ },
+ description: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:averageDistanceToProcessInKilometers.description',
+ defaultMessage: 'Áætluð meðalfjarlægð frá afgreiðslustöð.',
+ description: 'Lýsing á "Lengd ferðar í kílómetrum" reit',
+ },
+ }),
+ results: defineMessages({
+ calculate: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.calculate',
+ defaultMessage: 'Reikna',
+ description: 'Texti fyrir "Reikna" hnapp',
+ },
+ institutionGainDescription: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.institutionGainDescription',
+ defaultMessage: 'árlegur fjárhagslegur ávinningur stofnunar',
+ description: 'Lýsing á "ávinning stofnana" niðurstöðu',
+ },
+ staffFreeToDoOtherThings: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.staffFreeToDoOtherThings',
+ defaultMessage: 'ígildi stöðugildi sem nýtast í önnur verkefni',
+ description:
+ 'Lýsing á "hve margir starfsmenn geta gert annað" niðurstöðu',
+ },
+ citizenGainDescription: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.citizenGainDescription',
+ defaultMessage: 'heildarábati Íslands, ríki og borgara',
+ description: 'Lýsing á "ávinningur borgara" niðurstöðu',
+ },
+ ringRoadTripsSaved: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.ringRoadTripsSaved',
+ defaultMessage: 'keyrðar ferðir í kringum Ísland sem sparast',
+ description:
+ 'Lýsing á "keyrðar ferðir í kringum Ísland sem sparast" niðurstöðu',
+ },
+ savedCitizenDays: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.savedCitizenDays',
+ defaultMessage:
+ 'sparaðir hjá fólki við að sækja sér nauðsynlega þjónustu',
+ description:
+ 'Lýsing á "sparaðir dagar hjá fólki við að sækja sér nauðsynlega þjónustu" niðurstöðu',
+ },
+ c02: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.c02',
+ defaultMessage: 'minni losun Co2 vegna færri bílferða',
+ description: 'Lýsing á "minni losun Co2 vegna færri bílferða" niðurstöðu',
+ },
+ days: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.days',
+ defaultMessage: 'dagar',
+ description: 'Dagar',
+ },
+ currencyPostfix: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.currencyPostfix',
+ defaultMessage: ' kr.',
+ description: 'Viðskeyti eftir krónutölu',
+ },
+ kgPostfix: {
+ id: 'web.digitalIceland.benefitsOfDigitalProcesses:results.kgPostfix',
+ defaultMessage: ' kg',
+ description: 'Viðskeyti eftir kílometratölu í niðurstöuspjöldum',
+ },
+ }),
+}
diff --git a/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/utils.ts b/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/utils.ts
new file mode 100644
index 000000000000..fa9c4772f35e
--- /dev/null
+++ b/apps/web/components/connected/BenefitsOfDigitalProcessesCalculator/utils.ts
@@ -0,0 +1,124 @@
+import type { ConnectedComponent } from '@island.is/web/graphql/schema'
+
+export interface UserInput {
+ nameOfProcess: string
+ amountPerYear: number
+ processDurationInMinutes: number
+ visitCountToCompleteProcess: number
+ averageDistanceToProcessInKilometers: number
+}
+
+export interface Results {
+ /* Ávinningur stofnunar */
+ institutionGain: number
+
+ /* Ávinningur borgara */
+ citizenGain: number
+
+ /* Ígildi stöðugildis */
+ staffFreeToDoOtherThings: number
+
+ /* Eknir kílómetrar */
+ drivenKilometersSaved: number
+
+ /* Sparaðir dagar hjá fólki við að sækja sér þjónustu */
+ citizenTimeSaved: number
+}
+
+const avinningurR = (
+ laun: number,
+ f2f: number,
+ lengd: number,
+ magn: number,
+) => {
+ return (laun / 60) * f2f * lengd * magn
+}
+
+const avinningurB = (
+ fornarkostnadur: number,
+ lengd: number,
+ f2f: number,
+ km: number,
+ kmgjald: number,
+ okuhradi: number,
+ magn: number,
+) => {
+ return (
+ (f2f * 2 * km * kmgjald +
+ (60 / okuhradi) * km * 2 * f2f * (fornarkostnadur / 60) +
+ ((f2f * lengd) / 60) * fornarkostnadur) *
+ magn
+ )
+}
+
+export const calculateResults = (
+ slice: ConnectedComponent,
+ userInput: UserInput,
+) => {
+ const preConditions = {
+ staffIncomePerHour:
+ slice.configJson?.['Laun starfsmanna í framþjónustu krónur á klst'] ??
+ 6010,
+ citizenIncomeLossPerHour:
+ slice.configJson?.[
+ 'Fórnarkostnaður borgarar (meðallaun í landi á klst)'
+ ] ?? 5122,
+ kilometerFeePerKilometer: slice.configJson?.['Km gjald pr km'] ?? 141,
+ averageDrivingSpeedInKilometersPerHour:
+ slice.configJson?.['Meðalökuhraði km/klst'] ?? 40,
+ staffHourAverageInYear:
+ slice.configJson?.['Klukkustundir í stöðugildi á ári'] ?? 1606,
+ ringRoadDistanceInKilometers:
+ slice.configJson?.['Hringvegurinn í km'] ?? 1321,
+ kgCo2PerDrivenKilometer: slice.configJson?.['Kg co2 á ekinn km'] ?? 0.1082,
+ }
+
+ const results: Results = {
+ institutionGain: avinningurR(
+ preConditions.staffIncomePerHour,
+ userInput.visitCountToCompleteProcess,
+ userInput.processDurationInMinutes,
+ userInput.amountPerYear,
+ ),
+ citizenGain: avinningurB(
+ preConditions.citizenIncomeLossPerHour,
+ userInput.processDurationInMinutes,
+ userInput.visitCountToCompleteProcess,
+ userInput.averageDistanceToProcessInKilometers,
+ preConditions.kilometerFeePerKilometer,
+ preConditions.averageDrivingSpeedInKilometersPerHour,
+ userInput.amountPerYear,
+ ),
+ staffFreeToDoOtherThings:
+ (userInput.amountPerYear *
+ userInput.processDurationInMinutes *
+ userInput.visitCountToCompleteProcess) /
+ 60 /
+ preConditions.staffHourAverageInYear,
+ drivenKilometersSaved:
+ userInput.amountPerYear *
+ userInput.visitCountToCompleteProcess *
+ 2 *
+ userInput.averageDistanceToProcessInKilometers,
+ citizenTimeSaved:
+ (((userInput.visitCountToCompleteProcess *
+ 2 *
+ userInput.averageDistanceToProcessInKilometers *
+ 60) /
+ preConditions.averageDrivingSpeedInKilometersPerHour +
+ userInput.visitCountToCompleteProcess *
+ userInput.processDurationInMinutes) *
+ userInput.amountPerYear) /
+ 60 /
+ 24,
+ }
+
+ const gainPerCitizen = results.citizenGain + results.institutionGain
+ const ringRoadTripsSaved =
+ results.drivenKilometersSaved / preConditions.ringRoadDistanceInKilometers
+
+ const co2 =
+ preConditions.kgCo2PerDrivenKilometer * results.drivenKilometersSaved
+
+ return { results, gainPerCitizen, ringRoadTripsSaved, co2 }
+}
diff --git a/apps/web/utils/richText.tsx b/apps/web/utils/richText.tsx
index 837fffe1f256..3701edfb4a4e 100644
--- a/apps/web/utils/richText.tsx
+++ b/apps/web/utils/richText.tsx
@@ -79,6 +79,7 @@ import {
import { useI18n } from '@island.is/web/i18n'
import AdministrationOfOccupationalSafetyAndHealthCourses from '../components/connected/AdministrationOfOccupationalSafetyAndHealthCourses/AdministrationOfOccupationalSafetyAndHealthCourses'
+import { BenefitsOfDigitalProcessesCalculator } from '../components/connected/BenefitsOfDigitalProcessesCalculator/BenefitsOfDigitalProcessesCalculator'
import { MonthlyStatistics } from '../components/connected/electronicRegistrationStatistics'
import { GrindavikResidentialPropertyPurchaseCalculator } from '../components/connected/GrindavikResidentialPropertyPurchaseCalculator'
import HousingBenefitCalculator from '../components/connected/HousingBenefitCalculator/HousingBenefitCalculator/HousingBenefitCalculator'
@@ -191,6 +192,11 @@ export const webRenderConnectedComponent = (
case 'VMST/ParentalLeaveCalculator':
connectedComponent =
break
+ case 'DigitalIceland/BenefitsOfDigitalProcesses':
+ connectedComponent = (
+
+ )
+ break
default:
connectedComponent = renderConnectedComponent(slice)
}
diff --git a/charts/identity-server/values.dev.yaml b/charts/identity-server/values.dev.yaml
index 6795fd24a227..5f76287c7ee4 100644
--- a/charts/identity-server/values.dev.yaml
+++ b/charts/identity-server/values.dev.yaml
@@ -225,6 +225,8 @@ services-auth-admin-api:
LOG_LEVEL: 'info'
NODE_OPTIONS: '--max-old-space-size=691 -r dd-trace/init'
SERVERSIDE_FEATURES_ON: ''
+ SYSLUMENN_HOST: 'https://api.syslumenn.is/staging'
+ SYSLUMENN_TIMEOUT: '3000'
XROAD_BASE_PATH: 'http://securityserver.dev01.devland.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.dev01.devland.is/r1/IS-DEV'
XROAD_CLIENT_ID: 'IS-DEV/GOV/10000/island-is-client'
@@ -236,6 +238,7 @@ services-auth-admin-api:
XROAD_RSK_PROCURING_REDIS_NODES: '["clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379"]'
XROAD_TLS_BASE_PATH: 'https://securityserver.dev01.devland.is'
XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.dev01.devland.is/r1/IS-DEV'
+ ZENDESK_CONTACT_FORM_SUBDOMAIN: 'digitaliceland'
grantNamespaces:
- 'nginx-ingress-external'
- 'nginx-ingress-internal'
@@ -291,6 +294,10 @@ services-auth-admin-api:
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'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
+ ZENDESK_CONTACT_FORM_EMAIL: '/k8s/api/ZENDESK_CONTACT_FORM_EMAIL'
+ ZENDESK_CONTACT_FORM_TOKEN: '/k8s/api/ZENDESK_CONTACT_FORM_TOKEN'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -308,6 +315,8 @@ services-auth-delegation-api:
LOG_LEVEL: 'info'
NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init'
SERVERSIDE_FEATURES_ON: ''
+ SYSLUMENN_HOST: 'https://api.syslumenn.is/staging'
+ SYSLUMENN_TIMEOUT: '3000'
USER_NOTIFICATION_API_URL: 'http://web-user-notification.user-notification.svc.cluster.local'
XROAD_BASE_PATH: 'http://securityserver.dev01.devland.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.dev01.devland.is/r1/IS-DEV'
@@ -374,6 +383,8 @@ services-auth-delegation-api:
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'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -404,6 +415,8 @@ services-auth-ids-api:
PUBLIC_URL: 'https://identity-server.dev01.devland.is/api'
REDIS_NODES: '["clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379"]'
SERVERSIDE_FEATURES_ON: ''
+ SYSLUMENN_HOST: 'https://api.syslumenn.is/staging'
+ SYSLUMENN_TIMEOUT: '3000'
USER_PROFILE_CLIENT_SCOPE: '["@island.is/user-profile:read"]'
USER_PROFILE_CLIENT_URL: 'http://web-service-portal-api.service-portal.svc.cluster.local'
XROAD_BASE_PATH: 'http://securityserver.dev01.devland.is'
@@ -498,6 +511,8 @@ services-auth-ids-api:
NOVA_PASSWORD: '/k8s/services-auth/NOVA_PASSWORD'
NOVA_URL: '/k8s/services-auth/NOVA_URL'
NOVA_USERNAME: '/k8s/services-auth/NOVA_USERNAME'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -574,6 +589,8 @@ services-auth-personal-representative:
LOG_LEVEL: 'info'
NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init'
SERVERSIDE_FEATURES_ON: ''
+ SYSLUMENN_HOST: 'https://api.syslumenn.is/staging'
+ SYSLUMENN_TIMEOUT: '3000'
XROAD_BASE_PATH: 'http://securityserver.dev01.devland.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.dev01.devland.is/r1/IS-DEV'
XROAD_CLIENT_ID: 'IS-DEV/GOV/10000/island-is-client'
@@ -642,6 +659,8 @@ services-auth-personal-representative:
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'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -735,6 +754,8 @@ services-auth-public-api:
PUBLIC_URL: 'https://identity-server.dev01.devland.is/api'
REDIS_NODES: '["clustercfg.general-redis-cluster-group.5fzau3.euw1.cache.amazonaws.com:6379"]'
SERVERSIDE_FEATURES_ON: ''
+ SYSLUMENN_HOST: 'https://api.syslumenn.is/staging'
+ SYSLUMENN_TIMEOUT: '3000'
XROAD_BASE_PATH: 'http://securityserver.dev01.devland.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.dev01.devland.is/r1/IS-DEV'
XROAD_CLIENT_ID: 'IS-DEV/GOV/10000/island-is-client'
@@ -805,6 +826,8 @@ services-auth-public-api:
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'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
securityContext:
allowPrivilegeEscalation: false
privileged: false
diff --git a/charts/identity-server/values.prod.yaml b/charts/identity-server/values.prod.yaml
index 96bafb5febeb..1c3c4d8a443b 100644
--- a/charts/identity-server/values.prod.yaml
+++ b/charts/identity-server/values.prod.yaml
@@ -222,6 +222,8 @@ 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'
+ SYSLUMENN_HOST: 'https://api.syslumenn.is'
+ SYSLUMENN_TIMEOUT: '3000'
XROAD_BASE_PATH: 'http://securityserver.island.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.island.is/r1/IS'
XROAD_CLIENT_ID: 'IS/GOV/5501692829/island-is-client'
@@ -233,6 +235,7 @@ services-auth-admin-api:
XROAD_RSK_PROCURING_REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]'
XROAD_TLS_BASE_PATH: 'https://securityserver.island.is'
XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.island.is/r1/IS'
+ ZENDESK_CONTACT_FORM_SUBDOMAIN: 'digitaliceland'
grantNamespaces:
- 'nginx-ingress-external'
- 'nginx-ingress-internal'
@@ -288,6 +291,10 @@ services-auth-admin-api:
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'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
+ ZENDESK_CONTACT_FORM_EMAIL: '/k8s/api/ZENDESK_CONTACT_FORM_EMAIL'
+ ZENDESK_CONTACT_FORM_TOKEN: '/k8s/api/ZENDESK_CONTACT_FORM_TOKEN'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -305,6 +312,8 @@ services-auth-delegation-api:
LOG_LEVEL: 'info'
NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init'
SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms'
+ SYSLUMENN_HOST: 'https://api.syslumenn.is'
+ SYSLUMENN_TIMEOUT: '3000'
USER_NOTIFICATION_API_URL: 'https://user-notification.internal.island.is'
XROAD_BASE_PATH: 'http://securityserver.island.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.island.is/r1/IS'
@@ -371,6 +380,8 @@ services-auth-delegation-api:
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'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -401,6 +412,8 @@ services-auth-ids-api:
PUBLIC_URL: 'https://innskra.island.is/api'
REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]'
SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms'
+ SYSLUMENN_HOST: 'https://api.syslumenn.is'
+ SYSLUMENN_TIMEOUT: '3000'
USER_PROFILE_CLIENT_SCOPE: '["@island.is/user-profile:read"]'
USER_PROFILE_CLIENT_URL: 'https://service-portal-api.internal.island.is'
XROAD_BASE_PATH: 'http://securityserver.island.is'
@@ -495,6 +508,8 @@ services-auth-ids-api:
NOVA_PASSWORD: '/k8s/services-auth/NOVA_PASSWORD'
NOVA_URL: '/k8s/services-auth/NOVA_URL'
NOVA_USERNAME: '/k8s/services-auth/NOVA_USERNAME'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -571,6 +586,8 @@ services-auth-personal-representative:
LOG_LEVEL: 'info'
NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init'
SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms'
+ SYSLUMENN_HOST: 'https://api.syslumenn.is'
+ SYSLUMENN_TIMEOUT: '3000'
XROAD_BASE_PATH: 'http://securityserver.island.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.island.is/r1/IS'
XROAD_CLIENT_ID: 'IS/GOV/5501692829/island-is-client'
@@ -631,6 +648,8 @@ services-auth-personal-representative:
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'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -724,6 +743,8 @@ services-auth-public-api:
PUBLIC_URL: 'https://innskra.island.is/api'
REDIS_NODES: '["clustercfg.general-redis-cluster-group.dnugi2.euw1.cache.amazonaws.com:6379"]'
SERVERSIDE_FEATURES_ON: 'driving-license-use-v1-endpoint-for-v2-comms'
+ SYSLUMENN_HOST: 'https://api.syslumenn.is'
+ SYSLUMENN_TIMEOUT: '3000'
XROAD_BASE_PATH: 'http://securityserver.island.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.island.is/r1/IS'
XROAD_CLIENT_ID: 'IS/GOV/5501692829/island-is-client'
@@ -794,6 +815,8 @@ services-auth-public-api:
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'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
securityContext:
allowPrivilegeEscalation: false
privileged: false
diff --git a/charts/identity-server/values.staging.yaml b/charts/identity-server/values.staging.yaml
index 7c5eeccdd189..bee228ee9292 100644
--- a/charts/identity-server/values.staging.yaml
+++ b/charts/identity-server/values.staging.yaml
@@ -225,6 +225,8 @@ services-auth-admin-api:
LOG_LEVEL: 'info'
NODE_OPTIONS: '--max-old-space-size=691 -r dd-trace/init'
SERVERSIDE_FEATURES_ON: ''
+ SYSLUMENN_HOST: 'https://api.syslumenn.is/staging'
+ SYSLUMENN_TIMEOUT: '3000'
XROAD_BASE_PATH: 'http://securityserver.staging01.devland.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.staging01.devland.is/r1/IS-TEST'
XROAD_CLIENT_ID: 'IS-TEST/GOV/5501692829/island-is-client'
@@ -236,6 +238,7 @@ services-auth-admin-api:
XROAD_RSK_PROCURING_REDIS_NODES: '["clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379"]'
XROAD_TLS_BASE_PATH: 'https://securityserver.staging01.devland.is'
XROAD_TLS_BASE_PATH_WITH_ENV: 'https://securityserver.staging01.devland.is/r1/IS-TEST'
+ ZENDESK_CONTACT_FORM_SUBDOMAIN: 'digitaliceland'
grantNamespaces:
- 'nginx-ingress-external'
- 'nginx-ingress-internal'
@@ -291,6 +294,10 @@ services-auth-admin-api:
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'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
+ ZENDESK_CONTACT_FORM_EMAIL: '/k8s/api/ZENDESK_CONTACT_FORM_EMAIL'
+ ZENDESK_CONTACT_FORM_TOKEN: '/k8s/api/ZENDESK_CONTACT_FORM_TOKEN'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -308,6 +315,8 @@ services-auth-delegation-api:
LOG_LEVEL: 'info'
NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init'
SERVERSIDE_FEATURES_ON: ''
+ SYSLUMENN_HOST: 'https://api.syslumenn.is/staging'
+ SYSLUMENN_TIMEOUT: '3000'
USER_NOTIFICATION_API_URL: 'http://web-user-notification.user-notification.svc.cluster.local'
XROAD_BASE_PATH: 'http://securityserver.staging01.devland.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.staging01.devland.is/r1/IS-TEST'
@@ -374,6 +383,8 @@ services-auth-delegation-api:
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'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -404,6 +415,8 @@ services-auth-ids-api:
PUBLIC_URL: 'https://identity-server.staging01.devland.is/api'
REDIS_NODES: '["clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379"]'
SERVERSIDE_FEATURES_ON: ''
+ SYSLUMENN_HOST: 'https://api.syslumenn.is/staging'
+ SYSLUMENN_TIMEOUT: '3000'
USER_PROFILE_CLIENT_SCOPE: '["@island.is/user-profile:read"]'
USER_PROFILE_CLIENT_URL: 'http://web-service-portal-api.service-portal.svc.cluster.local'
XROAD_BASE_PATH: 'http://securityserver.staging01.devland.is'
@@ -498,6 +511,8 @@ services-auth-ids-api:
NOVA_PASSWORD: '/k8s/services-auth/NOVA_PASSWORD'
NOVA_URL: '/k8s/services-auth/NOVA_URL'
NOVA_USERNAME: '/k8s/services-auth/NOVA_USERNAME'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -574,6 +589,8 @@ services-auth-personal-representative:
LOG_LEVEL: 'info'
NODE_OPTIONS: '--max-old-space-size=460 -r dd-trace/init'
SERVERSIDE_FEATURES_ON: ''
+ SYSLUMENN_HOST: 'https://api.syslumenn.is/staging'
+ SYSLUMENN_TIMEOUT: '3000'
XROAD_BASE_PATH: 'http://securityserver.staging01.devland.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.staging01.devland.is/r1/IS-TEST'
XROAD_CLIENT_ID: 'IS-TEST/GOV/5501692829/island-is-client'
@@ -634,6 +651,8 @@ services-auth-personal-representative:
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'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
securityContext:
allowPrivilegeEscalation: false
privileged: false
@@ -727,6 +746,8 @@ services-auth-public-api:
PUBLIC_URL: 'https://identity-server.staging01.devland.is/api'
REDIS_NODES: '["clustercfg.general-redis-cluster-group.ab9ckb.euw1.cache.amazonaws.com:6379"]'
SERVERSIDE_FEATURES_ON: ''
+ SYSLUMENN_HOST: 'https://api.syslumenn.is/staging'
+ SYSLUMENN_TIMEOUT: '3000'
XROAD_BASE_PATH: 'http://securityserver.staging01.devland.is'
XROAD_BASE_PATH_WITH_ENV: 'http://securityserver.staging01.devland.is/r1/IS-TEST'
XROAD_CLIENT_ID: 'IS-TEST/GOV/5501692829/island-is-client'
@@ -797,6 +818,8 @@ services-auth-public-api:
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'
+ SYSLUMENN_PASSWORD: '/k8s/services-auth/SYSLUMENN_PASSWORD'
+ SYSLUMENN_USERNAME: '/k8s/services-auth/SYSLUMENN_USERNAME'
securityContext:
allowPrivilegeEscalation: false
privileged: false
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
index 37f89bac5db3..b871d2c40668 100644
--- 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
@@ -109,6 +109,16 @@ export class DelegationAdminResolver {
@Loader(DomainLoader) domainLoader: DomainDataLoader,
@Parent() delegation: DelegationDTO,
): Promise {
+ if (!delegation.domainName) {
+ return {
+ name: '',
+ displayName: '',
+ description: '',
+ nationalId: '',
+ organisationLogoKey: '',
+ }
+ }
+
const domainName = delegation.domainName ?? ISLAND_DOMAIN
const domain = await domainLoader.load({
lang: 'is',
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
index e6ce773980a3..00b3ee818f2b 100644
--- 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
@@ -42,15 +42,8 @@ export class DelegationAdminService {
}
async createDelegationAdmin(user: User, input: CreateDelegationInput) {
- // Mock response
- return Promise.resolve({
- id: 'some-id',
- fromNationalId: '0101302399',
- toNationalId: '0101302399',
- scopes: [],
- validTo: null,
- type: 'Custom',
- provider: 'delegationdb',
+ return this.delegationsWithAuth(user).delegationAdminControllerCreate({
+ createPaperDelegationDto: input,
})
}
}
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 af3cf9cc5cd4..91b22ada9926 100644
--- a/libs/api/domains/auth/src/lib/models/delegation.model.ts
+++ b/libs/api/domains/auth/src/lib/models/delegation.model.ts
@@ -56,6 +56,9 @@ export abstract class Delegation {
@Field(() => AuthDelegationProvider)
provider!: AuthDelegationProvider
+
+ @Field(() => String, { nullable: true })
+ referenceId?: string
}
@ObjectType('AuthLegalGuardianDelegation', {
diff --git a/libs/api/domains/health-directorate/src/lib/health-directorate.resolver.ts b/libs/api/domains/health-directorate/src/lib/health-directorate.resolver.ts
index 05f73ec5209d..da24d7f71039 100644
--- a/libs/api/domains/health-directorate/src/lib/health-directorate.resolver.ts
+++ b/libs/api/domains/health-directorate/src/lib/health-directorate.resolver.ts
@@ -60,9 +60,11 @@ export class HealthDirectorateResolver {
@Audit()
async updateDonorStatus(
@Args('input') input: DonorInput,
+ @Args('locale', { type: () => String, nullable: true })
+ locale: Locale = 'is',
@CurrentUser() user: User,
): Promise {
- return this.api.updateDonorStatus(user, input)
+ return this.api.updateDonorStatus(user, input, locale)
}
/* Vaccinations */
diff --git a/libs/api/domains/health-directorate/src/lib/health-directorate.service.ts b/libs/api/domains/health-directorate/src/lib/health-directorate.service.ts
index 4e40a778de6c..391a425c4bb0 100644
--- a/libs/api/domains/health-directorate/src/lib/health-directorate.service.ts
+++ b/libs/api/domains/health-directorate/src/lib/health-directorate.service.ts
@@ -24,8 +24,6 @@ export class HealthDirectorateService {
const lang: organLocale = locale === 'is' ? organLocale.Is : organLocale.En
const data: OrganDonorDto | null =
await this.organDonationApi.getOrganDonation(auth, lang)
- // Fetch organ list to get all names in correct language to sort out the names of the organs the user has limitations for
-
if (data === null) {
return null
}
@@ -58,11 +56,19 @@ export class HealthDirectorateService {
return limitations
}
- async updateDonorStatus(auth: Auth, input: DonorInput): Promise {
- return await this.organDonationApi.updateOrganDonation(auth, {
- isDonor: input.isDonor,
- exceptions: input.organLimitations ?? [],
- })
+ async updateDonorStatus(
+ auth: Auth,
+ input: DonorInput,
+ locale: Locale,
+ ): Promise {
+ return await this.organDonationApi.updateOrganDonation(
+ auth,
+ {
+ isDonor: input.isDonor,
+ exceptions: input.organLimitations ?? [],
+ },
+ locale === 'is' ? organLocale.Is : organLocale.En,
+ )
}
/* Vaccinations */
diff --git a/libs/api/domains/payment/src/lib/api-domains-payment.types.ts b/libs/api/domains/payment/src/lib/api-domains-payment.types.ts
index c07c61cf62dd..30ea95f06dbe 100644
--- a/libs/api/domains/payment/src/lib/api-domains-payment.types.ts
+++ b/libs/api/domains/payment/src/lib/api-domains-payment.types.ts
@@ -1,3 +1,6 @@
+import { IsEnum, IsString } from 'class-validator'
+import { ApiProperty } from '@nestjs/swagger'
+
export interface ChargeResult {
success: boolean
error: Error | null
@@ -14,8 +17,23 @@ export interface CallbackResult {
data?: Callback
}
-export interface Callback {
- receptionID: string
- chargeItemSubject: string
- status: 'paid' | 'cancelled' | 'recreated' | 'recreatedAndPaid'
+export enum PaidStatus {
+ paid = 'paid',
+ cancelled = 'cancelled',
+ recreated = 'recreated',
+ recreatedAndPaid = 'recreatedAndPaid',
+}
+
+export class Callback {
+ @IsString()
+ @ApiProperty()
+ readonly receptionID!: string
+
+ @IsString()
+ @ApiProperty()
+ readonly chargeItemSubject!: string
+
+ @IsEnum(PaidStatus)
+ @ApiProperty({ enum: PaidStatus })
+ readonly status!: PaidStatus
}
diff --git a/libs/api/domains/signature-collection/src/lib/decorators/acessRequirement.decorator.ts b/libs/api/domains/signature-collection/src/lib/decorators/acessRequirement.decorator.ts
deleted file mode 100644
index 7960279d7b76..000000000000
--- a/libs/api/domains/signature-collection/src/lib/decorators/acessRequirement.decorator.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { SetMetadata } from '@nestjs/common'
-
-export enum OwnerAccess {
- AllowActor = 'AllowActor',
- RestrictActor = 'RestrictActor',
-}
-export enum UserAccess {
- RestrictActor = 'RestrictActor',
-}
-export const AccessRequirement = (access?: OwnerAccess | UserAccess) =>
- SetMetadata('owner-access', access)
diff --git a/libs/api/domains/signature-collection/src/lib/decorators/allowDelegation.decorator.ts b/libs/api/domains/signature-collection/src/lib/decorators/allowDelegation.decorator.ts
new file mode 100644
index 000000000000..c4256e4aba75
--- /dev/null
+++ b/libs/api/domains/signature-collection/src/lib/decorators/allowDelegation.decorator.ts
@@ -0,0 +1,4 @@
+import { SetMetadata } from '@nestjs/common'
+import { ALLOW_DELEGATION_KEY } from '../guards/constants'
+
+export const AllowDelegation = () => SetMetadata(ALLOW_DELEGATION_KEY, true)
diff --git a/libs/api/domains/signature-collection/src/lib/decorators/index.ts b/libs/api/domains/signature-collection/src/lib/decorators/index.ts
new file mode 100644
index 000000000000..9d4ea4508ebd
--- /dev/null
+++ b/libs/api/domains/signature-collection/src/lib/decorators/index.ts
@@ -0,0 +1,16 @@
+import { IsOwner } from './isOwner.decorator'
+import { CurrentSignee, getCurrentSignee } from './signee.decorator'
+import { AllowDelegation } from './allowDelegation.decorator'
+import {
+ AllowManager,
+ RestrictGuarantor,
+} from './parliamentaryUserTypes.decorator'
+
+export {
+ AllowDelegation,
+ CurrentSignee,
+ IsOwner,
+ getCurrentSignee,
+ AllowManager,
+ RestrictGuarantor,
+}
diff --git a/libs/api/domains/signature-collection/src/lib/decorators/isOwner.decorator.ts b/libs/api/domains/signature-collection/src/lib/decorators/isOwner.decorator.ts
new file mode 100644
index 000000000000..4d62d9f5acc2
--- /dev/null
+++ b/libs/api/domains/signature-collection/src/lib/decorators/isOwner.decorator.ts
@@ -0,0 +1,4 @@
+import { SetMetadata } from '@nestjs/common'
+import { IS_OWNER_KEY } from '../guards/constants'
+
+export const IsOwner = () => SetMetadata(IS_OWNER_KEY, true)
diff --git a/libs/api/domains/signature-collection/src/lib/decorators/parliamentaryUserTypes.decorator.ts b/libs/api/domains/signature-collection/src/lib/decorators/parliamentaryUserTypes.decorator.ts
new file mode 100644
index 000000000000..299721b492ef
--- /dev/null
+++ b/libs/api/domains/signature-collection/src/lib/decorators/parliamentaryUserTypes.decorator.ts
@@ -0,0 +1,28 @@
+import { SetMetadata } from '@nestjs/common'
+import {
+ ALLOW_DELEGATION_KEY,
+ RESTRICT_GUARANTOR_KEY,
+} from '../guards/constants'
+// ---------------
+// ---Guarantor---
+// ---------------
+// A guarantor is a user in the signature collection system, aimed at parliamentary collections.
+// A guarantor (is: Ábyrgðaraðili) defined by Þjóðskrá Íslands as one of the following :
+// - A holder of procuration
+// OR - A direct candidate in the party ballot
+
+export const RestrictGuarantor = () => SetMetadata(RESTRICT_GUARANTOR_KEY, true)
+
+// ---------------
+// ----Manager----
+// ---------------
+// A manager is a user in the signature collection system, aimed at parliamentary collections.
+// A manager (is: Umsjónaraðili) defined by Þjóðskrá Íslands as one of the following:
+// - Individuals delegated to a company without having a procuration role
+// OR - Individuals delegated to a person (possibly a list owner)
+
+// This is the same as the allow_delegation rule so no new constants are needed
+export const AllowManager = () => SetMetadata(ALLOW_DELEGATION_KEY, true)
+
+// Assumptions: Guarantors have access to everything unless otherwise stated
+// Managers have access to nothing unless otherwise stated
diff --git a/libs/api/domains/signature-collection/src/lib/dto/canSign.input.ts b/libs/api/domains/signature-collection/src/lib/dto/canSignFromPaper.input.ts
similarity index 62%
rename from libs/api/domains/signature-collection/src/lib/dto/canSign.input.ts
rename to libs/api/domains/signature-collection/src/lib/dto/canSignFromPaper.input.ts
index 3f00b419735f..1aa493259781 100644
--- a/libs/api/domains/signature-collection/src/lib/dto/canSign.input.ts
+++ b/libs/api/domains/signature-collection/src/lib/dto/canSignFromPaper.input.ts
@@ -2,8 +2,11 @@ import { IsString } from 'class-validator'
import { Field, InputType } from '@nestjs/graphql'
@InputType()
-export class SignatureCollectionCanSignInput {
+export class SignatureCollectionCanSignFromPaperInput {
@Field()
@IsString()
signeeNationalId!: string
+ @Field()
+ @IsString()
+ listId!: string
}
diff --git a/libs/api/domains/signature-collection/src/lib/dto/index.ts b/libs/api/domains/signature-collection/src/lib/dto/index.ts
new file mode 100644
index 000000000000..a98035626847
--- /dev/null
+++ b/libs/api/domains/signature-collection/src/lib/dto/index.ts
@@ -0,0 +1,35 @@
+import { SignatureCollectionAddListsInput } from './addLists.input'
+import { SignatureCollectionAreaInput } from './area.input'
+import {
+ BulkUploadUser,
+ SignatureCollectionListBulkUploadInput,
+} from './bulkUpload.input'
+import { SignatureCollectionCandidateIdInput } from './candidateId.input'
+import { SignatureCollectionCancelListsInput } from './cencelLists.input'
+import { SignatureCollectionIdInput } from './collectionId.input'
+import { SignatureCollectionExtendDeadlineInput } from './extendDeadline.input'
+import { SignatureCollectionListIdInput } from './listId.input'
+import { SignatureCollectionNationalIdInput } from './nationalId.input'
+import { SignatureCollectionOwnerInput } from './owner.input'
+import { SignatureCollectionSignatureIdInput } from './signatureId.input'
+import { SignatureCollectionListInput } from './singatureList.input'
+import { SignatureCollectionUploadPaperSignatureInput } from './uploadPaperSignature.input'
+import { SignatureCollectionCanSignFromPaperInput } from './canSignFromPaper.input'
+
+export {
+ SignatureCollectionAddListsInput,
+ SignatureCollectionAreaInput,
+ SignatureCollectionListBulkUploadInput,
+ BulkUploadUser,
+ SignatureCollectionCandidateIdInput,
+ SignatureCollectionCancelListsInput,
+ SignatureCollectionIdInput,
+ SignatureCollectionExtendDeadlineInput,
+ SignatureCollectionListIdInput,
+ SignatureCollectionNationalIdInput,
+ SignatureCollectionOwnerInput,
+ SignatureCollectionSignatureIdInput,
+ SignatureCollectionListInput,
+ SignatureCollectionUploadPaperSignatureInput,
+ SignatureCollectionCanSignFromPaperInput,
+}
diff --git a/libs/api/domains/signature-collection/src/lib/dto/signatureUpdate.input.ts b/libs/api/domains/signature-collection/src/lib/dto/signatureUpdate.input.ts
new file mode 100644
index 000000000000..540738fa59c2
--- /dev/null
+++ b/libs/api/domains/signature-collection/src/lib/dto/signatureUpdate.input.ts
@@ -0,0 +1,10 @@
+import { IsNumber } from 'class-validator'
+import { Field, InputType } from '@nestjs/graphql'
+import { SignatureCollectionSignatureIdInput } from './signatureId.input'
+
+@InputType()
+export class SignatureCollectionSignatureUpdateInput extends SignatureCollectionSignatureIdInput {
+ @Field()
+ @IsNumber()
+ pageNumber!: number
+}
diff --git a/libs/api/domains/signature-collection/src/lib/guards/constants.ts b/libs/api/domains/signature-collection/src/lib/guards/constants.ts
new file mode 100644
index 000000000000..063d335ecd3d
--- /dev/null
+++ b/libs/api/domains/signature-collection/src/lib/guards/constants.ts
@@ -0,0 +1,3 @@
+export const IS_OWNER_KEY = 'is-owner'
+export const ALLOW_DELEGATION_KEY = 'allow-delegation'
+export const RESTRICT_GUARANTOR_KEY = 'restrict-guarantor'
diff --git a/libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.spec.ts b/libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.spec.ts
new file mode 100644
index 000000000000..b8aa3fc7a80c
--- /dev/null
+++ b/libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.spec.ts
@@ -0,0 +1,489 @@
+import { Resolver, Query, GraphQLModule } from '@nestjs/graphql'
+import { UserAccessGuard } from './userAccess.guard'
+import { INestApplication, UseGuards } from '@nestjs/common'
+import {
+ AllowDelegation,
+ IsOwner,
+ RestrictGuarantor,
+ AllowManager,
+} from '../decorators'
+import { Test } from '@nestjs/testing'
+import { ApolloDriver } from '@nestjs/apollo'
+import { ConfigModule } from '@nestjs/config'
+jest.mock('@island.is/auth-nest-tools', () => {
+ const original = jest.requireActual('@island.is/auth-nest-tools')
+ return {
+ ...original,
+ getRequest: jest.fn(),
+ }
+})
+import {
+ getRequest,
+ IdsUserGuard,
+ MockAuthGuard,
+ User,
+} from '@island.is/auth-nest-tools'
+import { createCurrentUser } from '@island.is/testing/fixtures'
+import request from 'supertest'
+import {
+ SignatureCollectionClientConfig,
+ SignatureCollectionClientModule,
+} from '@island.is/clients/signature-collection'
+import { SignatureCollectionService } from '../signatureCollection.service'
+import { IdsClientConfig, XRoadConfig } from '@island.is/nest/config'
+import { AuthDelegationType } from '@island.is/shared/types'
+
+const ownerNationalId = '0101303019'
+const ownerCompanyId = '0000000000'
+const someNationalId = '0101307789'
+const someCompanyId = '0000000001'
+
+const basicUser = createCurrentUser({
+ nationalIdType: 'person',
+ nationalId: someNationalId,
+})
+
+const authGuard = new MockAuthGuard(basicUser)
+const delegatedUserNotToOwner = createCurrentUser({
+ nationalIdType: 'person',
+ actor: { nationalId: someNationalId },
+})
+
+const delegatedUserToOwner = createCurrentUser({
+ nationalIdType: 'person',
+ actor: { nationalId: someNationalId },
+ nationalId: ownerNationalId,
+})
+
+const userIsOwnerNotDelegated = createCurrentUser({
+ nationalIdType: 'person',
+ nationalId: ownerNationalId,
+})
+
+const userHasProcurationAndIsOwner = createCurrentUser({
+ nationalIdType: 'company',
+ actor: { nationalId: someNationalId },
+ nationalId: ownerCompanyId,
+ delegationType: [AuthDelegationType.ProcurationHolder],
+})
+
+const userHasProcurationAndIsNotOwner = createCurrentUser({
+ nationalIdType: 'company',
+ actor: { nationalId: someNationalId },
+ nationalId: someCompanyId,
+ delegationType: [AuthDelegationType.ProcurationHolder],
+})
+
+const userDelegatedToCompanyButNotProcurationHolder = createCurrentUser({
+ nationalIdType: 'company',
+ actor: { nationalId: someNationalId },
+ nationalId: someCompanyId,
+ delegationType: [AuthDelegationType.Custom],
+})
+
+const okGraphQLResponse = (queryName: string) => ({
+ data: {
+ [queryName]: true,
+ },
+})
+
+const forbiddenGraphqlResponse = (queryName: string) => ({
+ data: {
+ [queryName]: null,
+ },
+ errors: [{ message: 'Forbidden resource' }],
+})
+
+@UseGuards(UserAccessGuard, IdsUserGuard)
+@Resolver()
+class TestResolver {
+ @Query(() => Boolean, { nullable: true })
+ @IsOwner()
+ getIfOwner() {
+ return true
+ }
+
+ @Query(() => Boolean, { nullable: true })
+ @IsOwner()
+ @AllowDelegation()
+ getIfOwnerWithDelegationAllowed() {
+ return true
+ }
+
+ @Query(() => Boolean, { nullable: true })
+ @AllowDelegation()
+ getIfAllowedDelegation() {
+ return true
+ }
+
+ @Query(() => Boolean, { nullable: true })
+ getForAllNonDelegatedUsers() {
+ return true
+ }
+
+ @Query(() => Boolean, { nullable: true })
+ @RestrictGuarantor()
+ getIsRestrictedToGuarantors() {
+ return true
+ }
+
+ @Query(() => Boolean, { nullable: true })
+ @AllowManager()
+ getIsAllowedForManagers() {
+ return true
+ }
+
+ @Query(() => Boolean, { nullable: true })
+ @RestrictGuarantor()
+ @AllowManager()
+ getIsRestrictedToGuarantorsAndAllowedForManagers() {
+ return true
+ }
+
+ @Query(() => Boolean, { nullable: true })
+ @IsOwner()
+ @AllowManager()
+ getIfOwnerWithAllowManager() {
+ return true
+ }
+}
+
+describe('UserAccessGuard', () => {
+ let app: INestApplication
+ let signatureCollectionService: SignatureCollectionService
+ const setupMockForUser = (user: User): void => {
+ jest.spyOn(authGuard, 'getAuth').mockReturnValue(user)
+ ;(getRequest as jest.Mock).mockReturnValue({
+ user,
+ })
+ }
+
+ beforeAll(async () => {
+ const moduleRef = await Test.createTestingModule({
+ providers: [TestResolver, SignatureCollectionService],
+ imports: [
+ GraphQLModule.forRoot({
+ autoSchemaFile: true,
+ driver: ApolloDriver,
+ path: '/graphql',
+ }),
+ ConfigModule.forRoot({
+ isGlobal: true,
+ load: [SignatureCollectionClientConfig, IdsClientConfig, XRoadConfig],
+ }),
+ SignatureCollectionClientModule,
+ ],
+ })
+ .overrideGuard(IdsUserGuard)
+ .useValue(authGuard)
+ .compile()
+
+ app = moduleRef.createNestApplication()
+ signatureCollectionService = app.get(
+ SignatureCollectionService,
+ )
+
+ await app.init()
+ })
+
+ beforeEach(() => {
+ jest
+ .spyOn(signatureCollectionService, 'signee')
+ .mockImplementation((user: User, _nationalId?: string) => {
+ return Promise.resolve({
+ canCreate: true,
+ canSign: true,
+ isOwner: [ownerNationalId, ownerCompanyId].includes(user.nationalId),
+ name: 'Test',
+ nationalId: user.nationalId,
+ candidate: {
+ id: '1',
+ name: 'Test',
+ nationalId: user.nationalId,
+ },
+ })
+ })
+
+ jest
+ .spyOn(signatureCollectionService, 'isCollector')
+ .mockImplementation(() => {
+ return Promise.resolve(true)
+ })
+ })
+
+ afterEach(() => {
+ jest.clearAllMocks()
+ jest.restoreAllMocks()
+ })
+
+ const gqlQuery = (query: string) =>
+ request(app.getHttpServer()).get('/graphql').query({
+ query,
+ })
+
+ it('Should allow owner to access IsOwner decorated paths', async () => {
+ setupMockForUser(userIsOwnerNotDelegated)
+
+ const response = await gqlQuery('{ getIfOwner }')
+
+ expect(response.body).toMatchObject(okGraphQLResponse('getIfOwner'))
+ })
+
+ it('Should not allow user delegated to owner to access IsOwner decorated paths without AllowDelegation', async () => {
+ setupMockForUser(delegatedUserToOwner)
+
+ const response = await gqlQuery('{ getIfOwner }')
+
+ expect(response.body).toMatchObject(forbiddenGraphqlResponse('getIfOwner'))
+ })
+
+ it('Should not allow basic users to access IsOwner when not owner', async () => {
+ setupMockForUser(basicUser)
+
+ const response = await gqlQuery('{ getIfOwner }')
+
+ expect(response.body).toMatchObject(forbiddenGraphqlResponse('getIfOwner'))
+ })
+
+ it('Where AllowDelegation and IsOwner: Should allow user delegated to owner', async () => {
+ setupMockForUser(delegatedUserToOwner)
+
+ const response = await gqlQuery('{ getIfOwnerWithDelegationAllowed }')
+
+ expect(response.body).toMatchObject(
+ okGraphQLResponse('getIfOwnerWithDelegationAllowed'),
+ )
+ })
+
+ it('Where AllowDelegation and IsOwner: Should not allow user delegated to non-owner', async () => {
+ setupMockForUser(delegatedUserNotToOwner)
+
+ const response = await gqlQuery('{ getIfOwnerWithDelegationAllowed }')
+
+ expect(response.body).toMatchObject(
+ forbiddenGraphqlResponse('getIfOwnerWithDelegationAllowed'),
+ )
+ })
+
+ it('With no decorators present: Should restrict delegation to owner', async () => {
+ setupMockForUser(delegatedUserToOwner)
+
+ const response = await gqlQuery('{ getForAllNonDelegatedUsers }')
+
+ expect(response.body).toMatchObject(
+ forbiddenGraphqlResponse('getForAllNonDelegatedUsers'),
+ )
+ })
+
+ it('With no decorators present: Should restrict delegation to non-owner', async () => {
+ setupMockForUser(delegatedUserNotToOwner)
+
+ const response = await gqlQuery('{ getForAllNonDelegatedUsers }')
+
+ expect(response.body).toMatchObject(
+ forbiddenGraphqlResponse('getForAllNonDelegatedUsers'),
+ )
+ })
+
+ it('With no decorators present: Should allow basic users', async () => {
+ setupMockForUser(basicUser)
+
+ const response = await gqlQuery('{ getForAllNonDelegatedUsers }')
+
+ expect(response.body).toMatchObject(
+ okGraphQLResponse('getForAllNonDelegatedUsers'),
+ )
+ })
+
+ it('When IsOwner not present: Should allow delegation with AllowDelegation for owner delegations', async () => {
+ setupMockForUser(delegatedUserToOwner)
+
+ const response = await gqlQuery('{ getIfAllowedDelegation }')
+
+ expect(response.body).toMatchObject(
+ okGraphQLResponse('getIfAllowedDelegation'),
+ )
+ })
+
+ it('When IsOwner not present: Should allow delegation with AllowDelegation for non-owner delegations', async () => {
+ setupMockForUser(delegatedUserNotToOwner)
+
+ const response = await gqlQuery('{ getIfAllowedDelegation }')
+
+ expect(response.body).toMatchObject(
+ okGraphQLResponse('getIfAllowedDelegation'),
+ )
+ })
+ it('With only AllowDelegation: Should not restrict basic users', async () => {
+ setupMockForUser(basicUser)
+
+ const response = await gqlQuery('{ getIfAllowedDelegation }')
+
+ expect(response.body).toMatchObject(
+ okGraphQLResponse('getIfAllowedDelegation'),
+ )
+ })
+
+ it('With only IsOwner: Should not restrict delegation of a procuration type even with no AllowDelegation when delegated to owner', async () => {
+ setupMockForUser(userHasProcurationAndIsOwner)
+
+ const response = await gqlQuery('{ getIfOwner }')
+
+ expect(response.body).toMatchObject(okGraphQLResponse('getIfOwner'))
+ })
+
+ it('With only IsOwner: Should restrict delegation of a procuration type even with no AllowDelegation when delegated to non-owner', async () => {
+ setupMockForUser(userHasProcurationAndIsNotOwner)
+
+ const response = await gqlQuery('{ getIfOwner }')
+
+ expect(response.body).toMatchObject(forbiddenGraphqlResponse('getIfOwner'))
+ })
+
+ it('With RestrictGuarantor: Should restrict access to guarantors', async () => {
+ setupMockForUser(userIsOwnerNotDelegated)
+
+ let response = await gqlQuery('{ getIsRestrictedToGuarantors }')
+
+ expect(response.body).toMatchObject(
+ forbiddenGraphqlResponse('getIsRestrictedToGuarantors'),
+ )
+
+ setupMockForUser(userHasProcurationAndIsOwner)
+
+ response = await gqlQuery('{ getIsRestrictedToGuarantors }')
+
+ expect(response.body).toMatchObject(
+ forbiddenGraphqlResponse('getIsRestrictedToGuarantors'),
+ )
+
+ setupMockForUser(userHasProcurationAndIsNotOwner)
+
+ response = await gqlQuery('{ getIsRestrictedToGuarantors }')
+
+ expect(response.body).toMatchObject(
+ forbiddenGraphqlResponse('getIsRestrictedToGuarantors'),
+ )
+ })
+
+ it('With RestrictGuarantor and AllowManager: Should allow access to managers', async () => {
+ setupMockForUser(userIsOwnerNotDelegated)
+
+ // DISALLOW ALL GUARANTORS
+ let response = await gqlQuery(
+ '{ getIsRestrictedToGuarantorsAndAllowedForManagers }',
+ )
+
+ expect(response.body).toMatchObject(
+ forbiddenGraphqlResponse(
+ 'getIsRestrictedToGuarantorsAndAllowedForManagers',
+ ),
+ )
+
+ setupMockForUser(userHasProcurationAndIsOwner)
+
+ response = await gqlQuery(
+ '{ getIsRestrictedToGuarantorsAndAllowedForManagers }',
+ )
+
+ expect(response.body).toMatchObject(
+ forbiddenGraphqlResponse(
+ 'getIsRestrictedToGuarantorsAndAllowedForManagers',
+ ),
+ )
+
+ setupMockForUser(userHasProcurationAndIsNotOwner)
+
+ response = await gqlQuery(
+ '{ getIsRestrictedToGuarantorsAndAllowedForManagers }',
+ )
+
+ expect(response.body).toMatchObject(
+ forbiddenGraphqlResponse(
+ 'getIsRestrictedToGuarantorsAndAllowedForManagers',
+ ),
+ )
+
+ // ALLOW ALL MANAGERS
+ setupMockForUser(delegatedUserNotToOwner)
+
+ response = await gqlQuery(
+ '{ getIsRestrictedToGuarantorsAndAllowedForManagers }',
+ )
+
+ expect(response.body).toMatchObject(
+ okGraphQLResponse('getIsRestrictedToGuarantorsAndAllowedForManagers'),
+ )
+
+ setupMockForUser(delegatedUserToOwner)
+
+ response = await gqlQuery(
+ '{ getIsRestrictedToGuarantorsAndAllowedForManagers }',
+ )
+
+ expect(response.body).toMatchObject(
+ okGraphQLResponse('getIsRestrictedToGuarantorsAndAllowedForManagers'),
+ )
+
+ setupMockForUser(userDelegatedToCompanyButNotProcurationHolder)
+
+ response = await gqlQuery(
+ '{ getIsRestrictedToGuarantorsAndAllowedForManagers }',
+ )
+
+ expect(response.body).toMatchObject(
+ okGraphQLResponse('getIsRestrictedToGuarantorsAndAllowedForManagers'),
+ )
+ })
+
+ it('With AllowManager: Should allow access to managers', async () => {
+ // ALLOW ALL MANAGERS
+ setupMockForUser(delegatedUserNotToOwner)
+
+ let response = await gqlQuery(
+ '{ getIsRestrictedToGuarantorsAndAllowedForManagers }',
+ )
+
+ expect(response.body).toMatchObject(
+ okGraphQLResponse('getIsRestrictedToGuarantorsAndAllowedForManagers'),
+ )
+
+ setupMockForUser(delegatedUserToOwner)
+
+ response = await gqlQuery(
+ '{ getIsRestrictedToGuarantorsAndAllowedForManagers }',
+ )
+
+ expect(response.body).toMatchObject(
+ okGraphQLResponse('getIsRestrictedToGuarantorsAndAllowedForManagers'),
+ )
+
+ setupMockForUser(userDelegatedToCompanyButNotProcurationHolder)
+
+ response = await gqlQuery(
+ '{ getIsRestrictedToGuarantorsAndAllowedForManagers }',
+ )
+
+ expect(response.body).toMatchObject(
+ okGraphQLResponse('getIsRestrictedToGuarantorsAndAllowedForManagers'),
+ )
+ })
+
+ it('Allow manager does not override the IsOwner decorator', async () => {
+ setupMockForUser(delegatedUserToOwner)
+
+ let response = await gqlQuery('{ getIfOwnerWithAllowManager }')
+
+ expect(response.body).toMatchObject(
+ okGraphQLResponse('getIfOwnerWithAllowManager'),
+ )
+
+ setupMockForUser(delegatedUserNotToOwner)
+
+ response = await gqlQuery('{ getIfOwnerWithAllowManager }')
+
+ expect(response.body).toMatchObject(
+ forbiddenGraphqlResponse('getIfOwnerWithAllowManager'),
+ )
+ })
+})
diff --git a/libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.ts b/libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.ts
index b3fbfa65c404..3f485e4a9528 100644
--- a/libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.ts
+++ b/libs/api/domains/signature-collection/src/lib/guards/userAccess.guard.ts
@@ -1,12 +1,22 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
-import { BYPASS_AUTH_KEY, getRequest } from '@island.is/auth-nest-tools'
-import {
- OwnerAccess,
- UserAccess,
-} from '../decorators/acessRequirement.decorator'
+import { BYPASS_AUTH_KEY, getRequest, User } from '@island.is/auth-nest-tools'
import { SignatureCollectionService } from '../signatureCollection.service'
+import { MetadataAbstractor } from '../utils'
import { AuthDelegationType } from '@island.is/shared/types'
+import { isPerson } from 'kennitala'
+import {
+ ALLOW_DELEGATION_KEY,
+ IS_OWNER_KEY,
+ RESTRICT_GUARANTOR_KEY,
+} from './constants'
+
+enum UserDelegationContext {
+ Person = 'Person',
+ PersonDelegatedToPerson = 'PersonDelegatedToPerson',
+ PersonDelegatedToCompany = 'PersonDelegatedToCompany',
+ ProcurationHolder = 'ProcurationHolder',
+}
@Injectable()
export class UserAccessGuard implements CanActivate {
@@ -14,56 +24,82 @@ export class UserAccessGuard implements CanActivate {
private reflector: Reflector,
private readonly signatureCollectionService: SignatureCollectionService,
) {}
+
+ private determineUserDelegationContext(
+ user: Express.User & User,
+ ): UserDelegationContext {
+ // If actor found on user, then user is delegated
+ if (user.actor?.nationalId) {
+ // If delegation is from person to person
+ if (isPerson(user.nationalId)) {
+ return UserDelegationContext.PersonDelegatedToPerson
+ } else {
+ // Determine whether it's a procuration vs delegation to a company
+ const hasProcuration = user.delegationType?.some(
+ (delegation) => delegation === AuthDelegationType.ProcurationHolder,
+ )
+
+ return hasProcuration
+ ? UserDelegationContext.ProcurationHolder
+ : UserDelegationContext.PersonDelegatedToCompany
+ }
+ }
+
+ return UserDelegationContext.Person
+ }
+
async canActivate(context: ExecutionContext): Promise {
- const bypassAuth = this.reflector.getAllAndOverride(
- BYPASS_AUTH_KEY,
- [context.getHandler(), context.getClass()],
+ const m = new MetadataAbstractor(this.reflector, context)
+ const isOwnerRestriction = m.getMetadataIfExists(IS_OWNER_KEY)
+ const bypassAuth = m.getMetadataIfExists(BYPASS_AUTH_KEY)
+ const allowDelegation = m.getMetadataIfExists(ALLOW_DELEGATION_KEY)
+ const restrictGuarantors = m.getMetadataIfExists(
+ RESTRICT_GUARANTOR_KEY,
)
- // if the bypass auth exists and is truthy we bypass auth
if (bypassAuth) {
return true
}
- const ownerRestriction = this.reflector.get(
- 'owner-access',
- context.getHandler(),
- )
- const request = getRequest(context)
+ const request = getRequest(context)
const user = request.user
if (!user) {
return false
}
- const isDelegatedUser = !!user?.actor?.nationalId
- const isProcurationHolder = user?.delegationType?.some(
- (delegation) => delegation === AuthDelegationType.ProcurationHolder,
- )
+ const delegationContext = this.determineUserDelegationContext(user)
+ const isDelegatedUser = [
+ UserDelegationContext.PersonDelegatedToCompany,
+ UserDelegationContext.PersonDelegatedToPerson,
+ ].includes(delegationContext)
+
+ if (isDelegatedUser && !allowDelegation) {
+ return false
+ }
+
+ if (restrictGuarantors && !isDelegatedUser) {
+ return false
+ }
+
// IsOwner needs signee
const signee = await this.signatureCollectionService.signee(user)
request.body = { ...request.body, signee }
- // IsOwner decorator not used
- if (!ownerRestriction) {
- return true
- }
- if (ownerRestriction === UserAccess.RestrictActor) {
- return isDelegatedUser ? false : true
- }
const { candidate } = signee
- if (signee.isOwner && candidate) {
- // Check if user is an actor for owner and if so check if registered collector, if not actor will be added as collector
- if (isDelegatedUser && ownerRestriction === OwnerAccess.AllowActor) {
- const isCollector = await this.signatureCollectionService.isCollector(
- candidate.id,
- user,
- )
- return isCollector
+ if (isOwnerRestriction) {
+ if (signee.isOwner && candidate) {
+ // Check if user is an actor for owner and if so check if registered collector, if not actor will be added as collector
+ if (isDelegatedUser && allowDelegation) {
+ const isCollector = await this.signatureCollectionService.isCollector(
+ candidate.id,
+ user,
+ )
+ return isCollector
+ }
}
- return true
+ return signee.isOwner
}
- // if the user is not owner we return false
- return false
+ return true
}
}
diff --git a/libs/api/domains/signature-collection/src/lib/models/index.ts b/libs/api/domains/signature-collection/src/lib/models/index.ts
new file mode 100644
index 000000000000..73e6f2d9ab85
--- /dev/null
+++ b/libs/api/domains/signature-collection/src/lib/models/index.ts
@@ -0,0 +1,59 @@
+import {
+ SignatureCollectionArea,
+ SignatureCollectionAreaBase,
+} from './area.model'
+import {
+ SignatureCollectionBulk,
+ SignatureCollectionNationalIds,
+} from './bulk.model'
+import { CanSignInfo } from './canSignInfo.model'
+import { SignatureCollectionCandidate } from './candidate.model'
+import { SignatureCollection } from './collection.model'
+import { SignatureCollectionCollector } from './collector.model'
+import { SignatureCollectionNationalIdError } from './nationalIdError.model'
+import { SignatureCollectionSignature } from './signature.model'
+import {
+ SignatureCollectionList,
+ SignatureCollectionListBase,
+ SignatureCollectionOwnedList,
+ SignatureCollectionSignedList,
+} from './signatureList.model'
+import {
+ SignatureCollectionCandidateLookUp,
+ SignatureCollectionSignee,
+ SignatureCollectionSigneeBase,
+} from './signee.model'
+import { SignatureCollectionSlug } from './slug.model'
+import {
+ CollectionStatus,
+ ListStatus,
+ SignatureCollectionListStatus,
+ SignatureCollectionStatus,
+} from './status.model'
+import { SignatureCollectionSuccess } from './success.model'
+
+export {
+ SignatureCollectionArea,
+ SignatureCollectionAreaBase,
+ SignatureCollectionBulk,
+ SignatureCollectionNationalIds,
+ CanSignInfo,
+ SignatureCollectionCandidate,
+ SignatureCollection,
+ SignatureCollectionCollector,
+ SignatureCollectionNationalIdError,
+ SignatureCollectionSignature,
+ SignatureCollectionList,
+ SignatureCollectionListBase,
+ SignatureCollectionOwnedList,
+ SignatureCollectionSignedList,
+ SignatureCollectionCandidateLookUp,
+ SignatureCollectionSignee,
+ SignatureCollectionSigneeBase,
+ SignatureCollectionSlug,
+ CollectionStatus,
+ ListStatus,
+ SignatureCollectionListStatus,
+ SignatureCollectionStatus,
+ SignatureCollectionSuccess,
+}
diff --git a/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts b/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts
index a1f783de22de..88300bbb9c23 100644
--- a/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts
+++ b/libs/api/domains/signature-collection/src/lib/signatureCollection.resolver.ts
@@ -10,30 +10,33 @@ import {
Scopes,
} from '@island.is/auth-nest-tools'
import { UseGuards } from '@nestjs/common'
-import { SignatureCollection } from './models/collection.model'
+import { Audit } from '@island.is/nest/audit'
+import { UserAccessGuard } from './guards/userAccess.guard'
+import { ApiScope } from '@island.is/auth/scopes'
+import {
+ SignatureCollectionAddListsInput,
+ SignatureCollectionCancelListsInput,
+ SignatureCollectionCanSignFromPaperInput,
+ SignatureCollectionIdInput,
+ SignatureCollectionListIdInput,
+ SignatureCollectionUploadPaperSignatureInput,
+} from './dto'
+import {
+ AllowDelegation,
+ AllowManager,
+ CurrentSignee,
+ IsOwner,
+} from './decorators'
import {
+ SignatureCollection,
+ SignatureCollectionCollector,
SignatureCollectionList,
SignatureCollectionListBase,
+ SignatureCollectionSignature,
SignatureCollectionSignedList,
-} from './models/signatureList.model'
-import { SignatureCollectionListIdInput } from './dto/listId.input'
-import { SignatureCollectionSignature } from './models/signature.model'
-import { SignatureCollectionSignee } from './models/signee.model'
-import { Audit } from '@island.is/nest/audit'
-import { UserAccessGuard } from './guards/userAccess.guard'
-import {
- AccessRequirement,
- OwnerAccess,
- UserAccess,
-} from './decorators/acessRequirement.decorator'
-import { CurrentSignee } from './decorators/signee.decorator'
-import { ApiScope } from '@island.is/auth/scopes'
-import { SignatureCollectionCancelListsInput } from './dto/cencelLists.input'
-import { SignatureCollectionIdInput } from './dto/collectionId.input'
-import { SignatureCollectionCanSignInput } from './dto/canSign.input'
-import { SignatureCollectionAddListsInput } from './dto/addLists.input'
-import { SignatureCollectionListBulkUploadInput } from './dto/bulkUpload.input'
-import { SignatureCollectionUploadPaperSignatureInput } from './dto/uploadPaperSignature.input'
+ SignatureCollectionSignee,
+} from './models'
+
@UseGuards(IdsUserGuard, ScopesGuard, UserAccessGuard)
@Resolver()
@Audit({ namespace: '@island.is/api/signature-collection' })
@@ -64,7 +67,8 @@ export class SignatureCollectionResolver {
}
@Scopes(ApiScope.signatureCollection)
- @AccessRequirement(OwnerAccess.AllowActor)
+ @AllowManager()
+ @IsOwner()
@Query(() => [SignatureCollectionList])
@Audit()
async signatureCollectionListsForOwner(
@@ -76,7 +80,6 @@ export class SignatureCollectionResolver {
}
@Scopes(ApiScope.signatureCollection)
- @AccessRequirement(UserAccess.RestrictActor)
@Query(() => [SignatureCollectionListBase])
@Audit()
async signatureCollectionListsForUser(
@@ -88,7 +91,8 @@ export class SignatureCollectionResolver {
}
@Scopes(ApiScope.signatureCollection)
- @AccessRequirement(OwnerAccess.AllowActor)
+ @IsOwner()
+ @AllowManager()
@Query(() => SignatureCollectionList)
@Audit()
async signatureCollectionList(
@@ -99,7 +103,6 @@ export class SignatureCollectionResolver {
}
@Scopes(ApiScope.signatureCollection)
- @AccessRequirement(UserAccess.RestrictActor)
@Query(() => [SignatureCollectionSignedList], { nullable: true })
@Audit()
async signatureCollectionSignedList(
@@ -109,7 +112,8 @@ export class SignatureCollectionResolver {
}
@Scopes(ApiScope.signatureCollection)
- @AccessRequirement(OwnerAccess.AllowActor)
+ @IsOwner()
+ @AllowManager()
@Query(() => [SignatureCollectionSignature], { nullable: true })
@Audit()
async signatureCollectionSignatures(
@@ -121,7 +125,6 @@ export class SignatureCollectionResolver {
@Scopes(ApiScope.signatureCollection)
@Query(() => SignatureCollectionSignee)
- @AccessRequirement(UserAccess.RestrictActor)
@Audit()
async signatureCollectionSignee(
@CurrentSignee() signee: SignatureCollectionSignee,
@@ -131,19 +134,17 @@ export class SignatureCollectionResolver {
@Scopes(ApiScope.signatureCollection)
@Query(() => Boolean)
- @AccessRequirement(OwnerAccess.AllowActor)
+ @IsOwner()
+ @AllowManager()
@Audit()
- async signatureCollectionCanSign(
- @Args('input') input: SignatureCollectionCanSignInput,
+ async signatureCollectionCanSignFromPaper(
+ @Args('input') input: SignatureCollectionCanSignFromPaperInput,
@CurrentUser() user: User,
): Promise {
- return (
- await this.signatureCollectionService.signee(user, input.signeeNationalId)
- ).canSign
+ return await this.signatureCollectionService.canSignFromPaper(user, input)
}
@Scopes(ApiScope.signatureCollection)
- @AccessRequirement(UserAccess.RestrictActor)
@Mutation(() => SignatureCollectionSuccess)
@Audit()
async signatureCollectionUnsign(
@@ -154,7 +155,7 @@ export class SignatureCollectionResolver {
}
@Scopes(ApiScope.signatureCollection)
- @AccessRequirement(OwnerAccess.RestrictActor)
+ @IsOwner()
@Mutation(() => SignatureCollectionSuccess)
@Audit()
async signatureCollectionCancel(
@@ -165,7 +166,7 @@ export class SignatureCollectionResolver {
}
@Scopes(ApiScope.signatureCollection)
- @AccessRequirement(OwnerAccess.RestrictActor)
+ @IsOwner()
@Mutation(() => SignatureCollectionSuccess)
@Audit()
async signatureCollectionAddAreas(
@@ -176,7 +177,7 @@ export class SignatureCollectionResolver {
}
@Scopes(ApiScope.signatureCollection)
- @AccessRequirement(OwnerAccess.RestrictActor)
+ @IsOwner()
@Mutation(() => SignatureCollectionSuccess)
@Audit()
async signatureCollectionUploadPaperSignature(
@@ -188,4 +189,18 @@ export class SignatureCollectionResolver {
user,
)
}
+
+ @Scopes(ApiScope.signatureCollection)
+ @IsOwner()
+ @Query(() => [SignatureCollectionCollector])
+ @Audit()
+ async signatureCollectionCollectors(
+ @CurrentUser() user: User,
+ @CurrentSignee() signee: SignatureCollectionSignee,
+ ): Promise {
+ return this.signatureCollectionService.collectors(
+ user,
+ signee.candidate?.id,
+ )
+ }
}
diff --git a/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts b/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts
index c7cd95b72e88..62fe983282a1 100644
--- a/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts
+++ b/libs/api/domains/signature-collection/src/lib/signatureCollection.service.ts
@@ -18,6 +18,9 @@ import { SignatureCollectionAddListsInput } from './dto/addLists.input'
import { SignatureCollectionOwnerInput } from './dto/owner.input'
import { SignatureCollectionListBulkUploadInput } from './dto/bulkUpload.input'
import { SignatureCollectionUploadPaperSignatureInput } from './dto/uploadPaperSignature.input'
+import { SignatureCollectionCanSignFromPaperInput } from './dto/canSignFromPaper.input'
+import { SignatureCollectionCandidateIdInput } from './dto/candidateId.input'
+import { SignatureCollectionCollector } from './models/collector.model'
@Injectable()
export class SignatureCollectionService {
@@ -142,4 +145,35 @@ export class SignatureCollectionService {
input,
)
}
+
+ async canSignFromPaper(
+ user: User,
+ input: SignatureCollectionCanSignFromPaperInput,
+ ): Promise {
+ const signee = await this.signatureCollectionClientService.getSignee(
+ user,
+ input.signeeNationalId,
+ )
+ const list = await this.list(input.listId, user)
+ // Current signatures should not prevent paper signatures
+ const canSign =
+ signee.canSign ||
+ (signee.canSignInfo?.length === 1 &&
+ signee.canSignInfo[0] === ReasonKey.AlreadySigned)
+
+ return canSign && list.area.id === signee.area?.id
+ }
+
+ async collectors(
+ user: User,
+ candidateId: string | undefined,
+ ): Promise {
+ if (!candidateId) {
+ return []
+ }
+ return await this.signatureCollectionClientService.getCollectors(
+ user,
+ candidateId,
+ )
+ }
}
diff --git a/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.resolver.ts b/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.resolver.ts
index 58f264dd8b90..210711f3113c 100644
--- a/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.resolver.ts
+++ b/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.resolver.ts
@@ -31,9 +31,10 @@ import { SignatureCollectionNationalIdInput } from './dto/nationalId.input'
import { SignatureCollectionSignatureIdInput } from './dto/signatureId.input'
import { SignatureCollectionIdInput } from './dto/collectionId.input'
import { SignatureCollectionCandidateIdInput } from './dto/candidateId.input'
-import { SignatureCollectionCanSignInput } from './dto/canSign.input'
+import { SignatureCollectionCanSignFromPaperInput } from './dto/canSignFromPaper.input'
import { ReasonKey } from '@island.is/clients/signature-collection'
import { CanSignInfo } from './models/canSignInfo.model'
+import { SignatureCollectionSignatureUpdateInput } from './dto/signatureUpdate.input'
@UseGuards(IdsUserGuard, ScopesGuard)
@Scopes(AdminPortalScope.signatureCollectionProcess)
@@ -53,7 +54,7 @@ export class SignatureCollectionAdminResolver {
async signatureCollectionAdminCanSignInfo(
@CurrentUser()
user: User,
- @Args('input') input: SignatureCollectionCanSignInput,
+ @Args('input') input: SignatureCollectionCanSignFromPaperInput,
): Promise {
const canSignInfo = await this.signatureCollectionService.getCanSignInfo(
user,
@@ -241,4 +242,16 @@ export class SignatureCollectionAdminResolver {
): Promise {
return this.signatureCollectionService.compareLists(input, user)
}
+
+ @Mutation(() => SignatureCollectionSuccess)
+ @Audit()
+ async signatureCollectionAdminUpdatePaperSignaturePageNumber(
+ @CurrentUser() user: User,
+ @Args('input') input: SignatureCollectionSignatureUpdateInput,
+ ): Promise {
+ return this.signatureCollectionService.updateSignaturePageNumber(
+ user,
+ input,
+ )
+ }
}
diff --git a/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.service.ts b/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.service.ts
index baa2b6bb7dad..10fa8f783fa0 100644
--- a/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.service.ts
+++ b/libs/api/domains/signature-collection/src/lib/signatureCollectionAdmin.service.ts
@@ -21,6 +21,7 @@ import { SignatureCollectionListBulkUploadInput } from './dto/bulkUpload.input'
import { SignatureCollectionSlug } from './models/slug.model'
import { SignatureCollectionListStatus } from './models/status.model'
import { SignatureCollectionIdInput } from './dto/collectionId.input'
+import { SignatureCollectionSignatureUpdateInput } from './dto/signatureUpdate.input'
@Injectable()
export class SignatureCollectionAdminService {
@@ -176,4 +177,15 @@ export class SignatureCollectionAdminService {
user,
)
}
+
+ async updateSignaturePageNumber(
+ user: User,
+ input: SignatureCollectionSignatureUpdateInput,
+ ): Promise {
+ return await this.signatureCollectionClientService.updateSignaturePageNumber(
+ user,
+ input.signatureId,
+ input.pageNumber,
+ )
+ }
}
diff --git a/libs/api/domains/signature-collection/src/lib/utils/MetadataAbstractor.ts b/libs/api/domains/signature-collection/src/lib/utils/MetadataAbstractor.ts
new file mode 100644
index 000000000000..92cdcf5089c0
--- /dev/null
+++ b/libs/api/domains/signature-collection/src/lib/utils/MetadataAbstractor.ts
@@ -0,0 +1,17 @@
+import { ExecutionContext } from '@nestjs/common'
+import { Reflector } from '@nestjs/core'
+
+export class MetadataAbstractor {
+ constructor(
+ private readonly reflector: Reflector,
+ private readonly context: ExecutionContext,
+ ) {}
+
+ public getMetadataIfExists = (key: string): T | null => {
+ const reflectorData = this.reflector.getAllAndOverride(key, [
+ this.context.getHandler(),
+ this.context.getClass(),
+ ])
+ return reflectorData ?? null
+ }
+}
diff --git a/libs/api/domains/signature-collection/src/lib/utils/index.ts b/libs/api/domains/signature-collection/src/lib/utils/index.ts
new file mode 100644
index 000000000000..af1e408a8ab7
--- /dev/null
+++ b/libs/api/domains/signature-collection/src/lib/utils/index.ts
@@ -0,0 +1,2 @@
+import { MetadataAbstractor } from './MetadataAbstractor'
+export { MetadataAbstractor }
diff --git a/libs/api/domains/signature-collection/tsconfig.spec.json b/libs/api/domains/signature-collection/tsconfig.spec.json
index 6668655fc397..f59491cc62a7 100644
--- a/libs/api/domains/signature-collection/tsconfig.spec.json
+++ b/libs/api/domains/signature-collection/tsconfig.spec.json
@@ -3,7 +3,10 @@
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"module": "commonjs",
- "types": ["jest", "node"]
+ "types": ["jest", "node"],
+ "noPropertyAccessFromIndexSignature": false,
+ "noImplicitOverride": false,
+ "noImplicitReturns": false
},
"include": [
"jest.config.ts",
diff --git a/libs/application/api/core/src/lib/application/application.service.ts b/libs/application/api/core/src/lib/application/application.service.ts
index b55415927be0..194a1bf50dfe 100644
--- a/libs/application/api/core/src/lib/application/application.service.ts
+++ b/libs/application/api/core/src/lib/application/application.service.ts
@@ -299,6 +299,7 @@ export class ApplicationService {
| 'applicantActors'
| 'draftTotalSteps'
| 'draftFinishedSteps'
+ | 'pruneAt'
>
>,
) {
diff --git a/libs/application/api/payment/src/lib/payment-callback.controller.ts b/libs/application/api/payment/src/lib/payment-callback.controller.ts
index 61505938a624..3333aaf6d1e7 100644
--- a/libs/application/api/payment/src/lib/payment-callback.controller.ts
+++ b/libs/application/api/payment/src/lib/payment-callback.controller.ts
@@ -1,15 +1,22 @@
import { Body, Controller, Param, Post, ParseUUIDPipe } from '@nestjs/common'
-import type { Callback } from '@island.is/api/domains/payment'
+import { Callback } from '@island.is/api/domains/payment'
import { PaymentService } from './payment.service'
+import { ApplicationService } from '@island.is/application/api/core'
+import { ApiTags } from '@nestjs/swagger'
+import addMonths from 'date-fns/addMonths'
+@ApiTags('payment-callback')
@Controller()
export class PaymentCallbackController {
- constructor(private readonly paymentService: PaymentService) {}
+ constructor(
+ private readonly paymentService: PaymentService,
+ private readonly applicationService: ApplicationService,
+ ) {}
@Post('application-payment/:applicationId/:id')
async paymentApproved(
- @Param('applicationId', new ParseUUIDPipe()) applicationId: string,
@Body() callback: Callback,
+ @Param('applicationId', new ParseUUIDPipe()) applicationId: string,
@Param('id', new ParseUUIDPipe()) id: string,
): Promise {
if (callback.status !== 'paid') {
@@ -21,5 +28,17 @@ export class PaymentCallbackController {
callback.receptionID,
applicationId,
)
+
+ const application = await this.applicationService.findOneById(applicationId)
+ if (application) {
+ const oneMonthFromNow = addMonths(new Date(), 1)
+ //Applications payment states are default to be pruned in 24 hours.
+ //If the application is paid, we want to hold on to it for longer in case we get locked in an error state.
+
+ await this.applicationService.update(applicationId, {
+ ...application,
+ pruneAt: oneMonthFromNow,
+ })
+ }
}
}
diff --git a/libs/application/core/src/lib/messages.ts b/libs/application/core/src/lib/messages.ts
index ff96c66b7026..27e1439cbd2b 100644
--- a/libs/application/core/src/lib/messages.ts
+++ b/libs/application/core/src/lib/messages.ts
@@ -421,6 +421,12 @@ export const coreErrorMessages = defineMessages({
defaultMessage: 'Sending umsóknar mistókst',
description: 'Message indicating submission after payment failed',
},
+ paymentSubmitFailedDescription: {
+ id: 'application.system:core.payment.submitTitle',
+ defaultMessage:
+ 'Villa hefur komið upp við áframhaldandi vinnslu. Vinsamlegast reynið aftur síðar. Ef villa endurtekur sig vinsamlegast hafið samband við island@island.is.',
+ description: 'Message indicating submission after payment failed',
+ },
applicationSubmitFailed: {
id: 'application.system:core.application.SubmitFailed',
defaultMessage: 'Sending umsóknar mistókst',
diff --git a/libs/application/template-api-modules/src/lib/modules/templates/aosh/change-machine-supervisor/change-machine-supervisor.utils.ts b/libs/application/template-api-modules/src/lib/modules/templates/aosh/change-machine-supervisor/change-machine-supervisor.utils.ts
index 566f17ad80ee..d32c280f7468 100644
--- a/libs/application/template-api-modules/src/lib/modules/templates/aosh/change-machine-supervisor/change-machine-supervisor.utils.ts
+++ b/libs/application/template-api-modules/src/lib/modules/templates/aosh/change-machine-supervisor/change-machine-supervisor.utils.ts
@@ -74,16 +74,8 @@ export const sendNotificationsToRecipients = async (
)
.catch((e) => {
errors.push(
- `Error sending email about submit application in application: ID: ${
- application.id
- },
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ `Error sending email about submit application in application: ID: ${application.id},
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -104,13 +96,7 @@ export const sendNotificationsToRecipients = async (
errors.push(
`Error sending sms about submit application to
a phonenumber in application: ID: ${application.id},
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ role: ${recipientList[i].role}`,
e,
)
})
diff --git a/libs/application/template-api-modules/src/lib/modules/templates/aosh/transfer-of-machine-ownership/transfer-of-machine-ownership.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/aosh/transfer-of-machine-ownership/transfer-of-machine-ownership.service.ts
index 14a2bb0b4b8e..fba8b32860bb 100644
--- a/libs/application/template-api-modules/src/lib/modules/templates/aosh/transfer-of-machine-ownership/transfer-of-machine-ownership.service.ts
+++ b/libs/application/template-api-modules/src/lib/modules/templates/aosh/transfer-of-machine-ownership/transfer-of-machine-ownership.service.ts
@@ -128,16 +128,8 @@ export class TransferOfMachineOwnershipTemplateService extends BaseTemplateApiSe
)
.catch((e) => {
this.logger.error(
- `Error sending email about submit application in application: ID: ${
- application.id
- },
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ `Error sending email about submit application in application: ID: ${application.id},
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -154,13 +146,7 @@ export class TransferOfMachineOwnershipTemplateService extends BaseTemplateApiSe
this.logger.error(
`Error sending sms about submit application to
a phonenumber in application: ID: ${application.id},
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -217,16 +203,8 @@ export class TransferOfMachineOwnershipTemplateService extends BaseTemplateApiSe
)
.catch((e) => {
this.logger.error(
- `Error sending email about initReview in application: ID: ${
- application.id
- },
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ `Error sending email about initReview in application: ID: ${application.id},
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -243,13 +221,7 @@ export class TransferOfMachineOwnershipTemplateService extends BaseTemplateApiSe
this.logger.error(
`Error sending sms about initReview to
a phonenumber in application: ID: ${application.id},
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -333,16 +305,8 @@ export class TransferOfMachineOwnershipTemplateService extends BaseTemplateApiSe
)
.catch((e) => {
this.logger.error(
- `Error sending email about rejectApplication in application: ID: ${
- application.id
- },
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ `Error sending email about rejectApplication in application: ID: ${application.id},
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -363,13 +327,7 @@ export class TransferOfMachineOwnershipTemplateService extends BaseTemplateApiSe
this.logger.error(
`Error sending sms about rejectApplication to
a phonenumber in application: ID: ${application.id},
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ role: ${recipientList[i].role}`,
e,
)
})
diff --git a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-co-owner-of-vehicle/change-co-owner-of-vehicle.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-co-owner-of-vehicle/change-co-owner-of-vehicle.service.ts
index 4354a5020f61..3b75a3530164 100644
--- a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-co-owner-of-vehicle/change-co-owner-of-vehicle.service.ts
+++ b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-co-owner-of-vehicle/change-co-owner-of-vehicle.service.ts
@@ -281,16 +281,8 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService {
)
.catch((e) => {
this.logger.error(
- `Error sending email about initReview in application: ID: ${
- application.id
- },
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ `Error sending email about initReview in application: ID: ${application.id},
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -307,13 +299,7 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService {
this.logger.error(
`Error sending sms about initReview to
a phonenumber in application: ID: ${application.id},
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -355,16 +341,8 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService {
)
.catch((e) => {
this.logger.error(
- `Error sending email about rejectApplication in application: ID: ${
- application.id
- },
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ `Error sending email about rejectApplication in application: ID: ${application.id},
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -385,13 +363,7 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService {
this.logger.error(
`Error sending sms about rejectApplication to
a phonenumber in application: ID: ${application.id},
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -512,16 +484,8 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService {
)
.catch(() => {
this.logger.error(
- `Error sending email about submitApplication in application: ID: ${
- application.id
- },
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ `Error sending email about submitApplication in application: ID: ${application.id},
+ role: ${recipientList[i].role}`,
)
})
}
@@ -537,13 +501,7 @@ export class ChangeCoOwnerOfVehicleService extends BaseTemplateApiService {
this.logger.error(
`Error sending sms about submitApplication to
a phonenumber in application: ID: ${application.id},
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ role: ${recipientList[i].role}`,
e,
)
})
diff --git a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-operator-of-vehicle/change-operator-of-vehicle.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-operator-of-vehicle/change-operator-of-vehicle.service.ts
index ea24278392a1..7bdc09ff081f 100644
--- a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-operator-of-vehicle/change-operator-of-vehicle.service.ts
+++ b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/change-operator-of-vehicle/change-operator-of-vehicle.service.ts
@@ -253,16 +253,8 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService {
)
.catch((e) => {
this.logger.error(
- `Error sending email about initReview in application: ID: ${
- application.id
- },
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ `Error sending email about initReview in application: ID: ${application.id},
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -279,13 +271,7 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService {
this.logger.error(
`Error sending sms about initReview to
a phonenumber in application: ID: ${application.id},
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -327,16 +313,8 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService {
)
.catch((e) => {
this.logger.error(
- `Error sending email about rejectApplication in application: ID: ${
- application.id
- },
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ `Error sending email about rejectApplication in application: ID: ${application.id},
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -357,13 +335,7 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService {
this.logger.error(
`Error sending sms about rejectApplication to
a phonenumber in application: ID: ${application.id},
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -461,16 +433,8 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService {
)
.catch((e) => {
this.logger.error(
- `Error sending email about submitApplication in application: ID: ${
- application.id
- },
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ `Error sending email about submitApplication in application: ID: ${application.id},
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -487,13 +451,7 @@ export class ChangeOperatorOfVehicleService extends BaseTemplateApiService {
this.logger.error(
`Error sending sms about submitApplication to
a phonenumber in application: ID: ${application.id},
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ role: ${recipientList[i].role}`,
e,
)
})
diff --git a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/transfer-of-vehicle-ownership/transfer-of-vehicle-ownership.service.ts b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/transfer-of-vehicle-ownership/transfer-of-vehicle-ownership.service.ts
index eaaae0abe751..4d265b85aebd 100644
--- a/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/transfer-of-vehicle-ownership/transfer-of-vehicle-ownership.service.ts
+++ b/libs/application/template-api-modules/src/lib/modules/templates/transport-authority/transfer-of-vehicle-ownership/transfer-of-vehicle-ownership.service.ts
@@ -283,16 +283,8 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService {
)
.catch((e) => {
this.logger.error(
- `Error sending email about initReview in application: ID: ${
- application.id
- },
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ `Error sending email about initReview in application: ID: ${application.id},
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -309,13 +301,7 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService {
this.logger.error(
`Error sending sms about initReview to
a phonenumber in application: ID: ${application.id},
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -423,16 +409,8 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService {
)
.catch((e) => {
this.logger.error(
- `Error sending email about addReview in application: ID: ${
- application.id
- },
- role: ${
- newlyAddedRecipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === newlyAddedRecipientList[i].ssn,
- )}`
- }`,
+ `Error sending email about addReview in application: ID: ${application.id},
+ role: ${newlyAddedRecipientList[i].role}`,
e,
)
})
@@ -452,13 +430,7 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService {
this.logger.error(
`Error sending sms about addReview to
a phonenumber in application: ID: ${application.id},
- role: ${
- newlyAddedRecipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === newlyAddedRecipientList[i].ssn,
- )}`
- }`,
+ role: ${newlyAddedRecipientList[i].role}`,
e,
)
})
@@ -498,16 +470,8 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService {
)
.catch((e) => {
this.logger.error(
- `Error sending email about rejectApplication in application: ID: ${
- application.id
- },
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ `Error sending email about rejectApplication in application: ID: ${application.id},
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -528,13 +492,7 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService {
this.logger.error(
`Error sending sms about rejectApplication to
a phonenumber in application: ID: ${application.id},
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -641,16 +599,8 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService {
)
.catch((e) => {
this.logger.error(
- `Error sending email about submitApplication in application: ID: ${
- application.id
- },
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ `Error sending email about submitApplication in application: ID: ${application.id},
+ role: ${recipientList[i].role}`,
e,
)
})
@@ -667,13 +617,7 @@ export class TransferOfVehicleOwnershipService extends BaseTemplateApiService {
this.logger.error(
`Error sending sms about rejectApplication to
a phonenumber in application: ID: ${application.id},
- role: ${
- recipientList[i].ssn === application.applicant
- ? 'Applicant'
- : `Assignee index ${application.assignees.findIndex(
- (assignee) => assignee === recipientList[i].ssn,
- )}`
- }`,
+ role: ${recipientList[i].role}`,
e,
)
})
diff --git a/libs/application/templates/aosh/transfer-of-machine-ownership/src/fields/StopBuyerIfSameAsSeller/index.tsx b/libs/application/templates/aosh/transfer-of-machine-ownership/src/fields/StopBuyerIfSameAsSeller/index.tsx
new file mode 100644
index 000000000000..02058dbd6d1e
--- /dev/null
+++ b/libs/application/templates/aosh/transfer-of-machine-ownership/src/fields/StopBuyerIfSameAsSeller/index.tsx
@@ -0,0 +1,18 @@
+import { FieldBaseProps } from '@island.is/application/types'
+import { FC } from 'react'
+import { doSellerAndBuyerHaveSameNationalId } from '../../utils'
+
+export const StopBuyerIfSameAsSeller: FC<
+ React.PropsWithChildren
+> = (props) => {
+ const { application, setBeforeSubmitCallback } = props
+
+ setBeforeSubmitCallback?.(async () => {
+ if (doSellerAndBuyerHaveSameNationalId(application.answers)) {
+ return [false, '']
+ }
+ return [true, null]
+ })
+
+ return <>>
+}
diff --git a/libs/application/templates/aosh/transfer-of-machine-ownership/src/fields/index.ts b/libs/application/templates/aosh/transfer-of-machine-ownership/src/fields/index.ts
index 9afdf0c6eec2..60e8c6a65c3c 100644
--- a/libs/application/templates/aosh/transfer-of-machine-ownership/src/fields/index.ts
+++ b/libs/application/templates/aosh/transfer-of-machine-ownership/src/fields/index.ts
@@ -1,3 +1,4 @@
export { ApplicationStatus } from './ApplicationStatus'
export { MachinesField } from './MachinesField'
export { Review } from './Review'
+export { StopBuyerIfSameAsSeller } from './StopBuyerIfSameAsSeller'
diff --git a/libs/application/templates/aosh/transfer-of-machine-ownership/src/forms/TransferOfMachineOwnershipForm/InformationSection/buyerSubSection.ts b/libs/application/templates/aosh/transfer-of-machine-ownership/src/forms/TransferOfMachineOwnershipForm/InformationSection/buyerSubSection.ts
index c7f75e4884dd..80fc500d3237 100644
--- a/libs/application/templates/aosh/transfer-of-machine-ownership/src/forms/TransferOfMachineOwnershipForm/InformationSection/buyerSubSection.ts
+++ b/libs/application/templates/aosh/transfer-of-machine-ownership/src/forms/TransferOfMachineOwnershipForm/InformationSection/buyerSubSection.ts
@@ -1,12 +1,15 @@
import {
+ buildAlertMessageField,
+ buildCustomField,
buildMultiField,
buildNationalIdWithNameField,
buildPhoneField,
buildSubSection,
- buildSubmitField,
buildTextField,
} from '@island.is/application/core'
import { information } from '../../../lib/messages'
+import { FormValue } from '@island.is/application/types'
+import { doSellerAndBuyerHaveSameNationalId } from '../../../utils'
export const buyerSubSection = buildSubSection({
id: 'buyer',
@@ -35,6 +38,19 @@ export const buyerSubSection = buildSubSection({
width: 'half',
required: true,
}),
+ buildAlertMessageField({
+ id: 'buyer.alertMessage',
+ alertType: 'warning',
+ title: information.labels.buyer.alertTitle,
+ message: information.labels.buyer.alertMessage,
+ condition: (answer: FormValue) =>
+ doSellerAndBuyerHaveSameNationalId(answer),
+ }),
+ buildCustomField({
+ id: 'buyer.custom',
+ title: '',
+ component: 'StopBuyerIfSameAsSeller',
+ }),
],
}),
],
diff --git a/libs/application/templates/aosh/transfer-of-machine-ownership/src/lib/messages/information.ts b/libs/application/templates/aosh/transfer-of-machine-ownership/src/lib/messages/information.ts
index 5678775a9c72..844d836dfa18 100644
--- a/libs/application/templates/aosh/transfer-of-machine-ownership/src/lib/messages/information.ts
+++ b/libs/application/templates/aosh/transfer-of-machine-ownership/src/lib/messages/information.ts
@@ -266,6 +266,17 @@ export const information = {
defaultMessage: 'Staðfesta',
description: 'Submit button for buyer',
},
+ alertTitle: {
+ id: 'aosh.tmo.application:information.labels.buyer.alertTitle',
+ defaultMessage: 'Kennitala sú sama og hjá seljanda',
+ description: `Buyer alert title`,
+ },
+ alertMessage: {
+ id: 'aosh.tmo.application:information.labels.buyer.alertMessage',
+ defaultMessage:
+ 'Seljandi og kaupandi getur ekki verið sá sami. Vinsamlega skráðu nýja kennitölu.',
+ description: `Buyer alert message`,
+ },
}),
buyerOperators: defineMessages({
title: {
diff --git a/libs/application/templates/aosh/transfer-of-machine-ownership/src/utils/doSellerAndBuyerHaveSameNationalId.ts b/libs/application/templates/aosh/transfer-of-machine-ownership/src/utils/doSellerAndBuyerHaveSameNationalId.ts
new file mode 100644
index 000000000000..3113fbe45951
--- /dev/null
+++ b/libs/application/templates/aosh/transfer-of-machine-ownership/src/utils/doSellerAndBuyerHaveSameNationalId.ts
@@ -0,0 +1,17 @@
+import { getValueViaPath } from '@island.is/application/core'
+import { FormValue } from '@island.is/application/types'
+
+export const doSellerAndBuyerHaveSameNationalId = (answers: FormValue) => {
+ const buyerNationalId = getValueViaPath(
+ answers,
+ 'buyer.nationalId',
+ '',
+ ) as string
+ const sellerNationalId = getValueViaPath(
+ answers,
+ 'seller.nationalId',
+ '',
+ ) as string
+
+ return buyerNationalId === sellerNationalId
+}
diff --git a/libs/application/templates/aosh/transfer-of-machine-ownership/src/utils/index.ts b/libs/application/templates/aosh/transfer-of-machine-ownership/src/utils/index.ts
index e5350d91740c..d4364c5a10a6 100644
--- a/libs/application/templates/aosh/transfer-of-machine-ownership/src/utils/index.ts
+++ b/libs/application/templates/aosh/transfer-of-machine-ownership/src/utils/index.ts
@@ -15,6 +15,7 @@ export { getReviewSteps } from './getReviewSteps'
export { hasReviewerApproved } from './hasReviewerApproved'
export { getApproveAnswers } from './getApproveAnswers'
export { getRejecter } from './getRejecter'
+export { doSellerAndBuyerHaveSameNationalId } from './doSellerAndBuyerHaveSameNationalId'
export const getChargeItemCodes = (
application: Application,
diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddCommitteeMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddCommitteeMember.tsx
index d094f6b8e98e..26c31423553b 100644
--- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddCommitteeMember.tsx
+++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddCommitteeMember.tsx
@@ -9,6 +9,7 @@ import {
} from '../../lib/constants'
import { getCommitteeAnswers, getEmptyMember } from '../../lib/utils'
import set from 'lodash/set'
+import { useFormContext } from 'react-hook-form'
type Props = {
applicationId: string
@@ -20,6 +21,8 @@ export const AddCommitteeMember = ({ applicationId }: Props) => {
applicationId,
})
+ const { setValue } = useFormContext()
+
const onAddCommitteeMember = () => {
const { signature, currentAnswers } = getCommitteeAnswers(
structuredClone(application.answers),
@@ -37,6 +40,8 @@ export const AddCommitteeMember = ({ applicationId }: Props) => {
withExtraMember,
)
+ setValue(InputFields.signature.committee, withExtraMember)
+
updateApplication(updatedAnswers)
}
}
diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularMember.tsx
index 4d6aeb79bfc4..840c0aa11962 100644
--- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularMember.tsx
+++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularMember.tsx
@@ -9,6 +9,7 @@ import {
} from '../../lib/constants'
import { getEmptyMember, getRegularAnswers } from '../../lib/utils'
import set from 'lodash/set'
+import { useFormContext } from 'react-hook-form'
type Props = {
applicationId: string
@@ -21,6 +22,8 @@ export const AddRegularMember = ({ applicationId, signatureIndex }: Props) => {
applicationId,
})
+ const { setValue } = useFormContext()
+
const onAddMember = () => {
const { signature, currentAnswers } = getRegularAnswers(
structuredClone(application.answers),
@@ -47,6 +50,8 @@ export const AddRegularMember = ({ applicationId, signatureIndex }: Props) => {
updatedRegularSignature,
)
+ setValue(InputFields.signature.regular, updatedRegularSignature)
+
updateApplication(updatedAnswers)
}
}
diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularSignature.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularSignature.tsx
index 1d3ce373b3ed..7eaa66895463 100644
--- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularSignature.tsx
+++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/AddRegularSignature.tsx
@@ -15,6 +15,7 @@ import {
MAXIMUM_REGULAR_SIGNATURE_COUNT,
ONE,
} from '../../lib/constants'
+import { useFormContext } from 'react-hook-form'
type Props = {
applicationId: string
@@ -26,6 +27,8 @@ export const AddRegularSignature = ({ applicationId }: Props) => {
applicationId,
})
+ const { setValue } = useFormContext()
+
const onAddInstitution = () => {
const { signature, currentAnswers } = getRegularAnswers(
structuredClone(application.answers),
@@ -37,12 +40,16 @@ export const AddRegularSignature = ({ applicationId }: Props) => {
DEFAULT_REGULAR_SIGNATURE_MEMBER_COUNT,
)?.pop()
+ const updatedSignature = [...signature, newSignature]
+
const updatedAnswers = set(
currentAnswers,
InputFields.signature.regular,
- [...signature, newSignature],
+ updatedSignature,
)
+ setValue(InputFields.signature.regular, updatedSignature)
+
updateApplication(updatedAnswers)
}
}
diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx
index e95a9acbb706..3b0362ccb297 100644
--- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx
+++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Chairman.tsx
@@ -13,6 +13,7 @@ import { SignatureMember } from './Member'
import set from 'lodash/set'
import * as styles from './Signatures.css'
import * as z from 'zod'
+import { useFormContext } from 'react-hook-form'
type Props = {
applicationId: string
@@ -27,6 +28,8 @@ export const Chairman = ({ applicationId, member }: Props) => {
applicationId,
})
+ const { setValue } = useFormContext()
+
const handleChairmanChange = (value: string, key: keyof MemberProperties) => {
const { signature, currentAnswers } = getCommitteeAnswers(
application.answers,
@@ -57,6 +60,8 @@ export const Chairman = ({ applicationId, member }: Props) => {
updatedCommitteeSignature,
)
+ setValue(InputFields.signature.committee, updatedCommitteeSignature)
+
return updatedSignatures
}
diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx
index a269ded1ae8b..ae96183ff5bc 100644
--- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx
+++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/CommitteeMember.tsx
@@ -14,6 +14,7 @@ import { memberItemSchema } from '../../lib/dataSchema'
import { SignatureMember } from './Member'
import * as z from 'zod'
import { RemoveCommitteeMember } from './RemoveComitteeMember'
+import { useFormContext } from 'react-hook-form'
type Props = {
applicationId: string
@@ -33,6 +34,8 @@ export const CommitteeMember = ({
applicationId,
})
+ const { setValue } = useFormContext()
+
const handleMemberChange = (
value: string,
key: keyof MemberProperties,
@@ -77,6 +80,8 @@ export const CommitteeMember = ({
updatedCommitteeSignature,
)
+ setValue(InputFields.signature.committee, updatedCommitteeSignature)
+
return updatedSignatures
}
diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx
index 5a70d5b1512e..1f73a2f8f921 100644
--- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx
+++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/Institution.tsx
@@ -27,6 +27,7 @@ import {
import { z } from 'zod'
import { signatureInstitutionSchema } from '../../lib/dataSchema'
import { RemoveRegularSignature } from './RemoveRegularSignature'
+import { useFormContext } from 'react-hook-form'
type Props = {
applicationId: string
type: SignatureType
@@ -49,6 +50,8 @@ export const InstitutionSignature = ({
applicationId,
})
+ const { setValue } = useFormContext()
+
const handleInstitutionChange = (
value: string,
key: SignatureInstitutionKeys,
@@ -88,6 +91,8 @@ export const InstitutionSignature = ({
updatedRegularSignature,
)
+ setValue(InputFields.signature[type], updatedRegularSignature)
+
return updatedSignatures
}
@@ -114,6 +119,8 @@ export const InstitutionSignature = ({
},
)
+ setValue(InputFields.signature[type], updatedCommitteeSignature)
+
return updatedCommitteeSignature
}
diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx
index d1e48b57c55c..443bdb010e7d 100644
--- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx
+++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RegularMember.tsx
@@ -15,6 +15,7 @@ import { memberItemSchema } from '../../lib/dataSchema'
import { SignatureMember } from './Member'
import * as z from 'zod'
import { RemoveRegularMember } from './RemoveRegularMember'
+import { useFormContext } from 'react-hook-form'
type Props = {
applicationId: string
@@ -36,6 +37,8 @@ export const RegularMember = ({
applicationId,
})
+ const { setValue } = useFormContext()
+
const handleMemberChange = (
value: string,
key: keyof MemberProperties,
@@ -84,6 +87,8 @@ export const RegularMember = ({
updatedRegularSignature,
)
+ setValue(InputFields.signature.regular, updatedRegularSignature)
+
return updatedSignatures
}
diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveComitteeMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveComitteeMember.tsx
index 43d6f6ba4295..941aa82bf009 100644
--- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveComitteeMember.tsx
+++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveComitteeMember.tsx
@@ -5,6 +5,7 @@ import { MINIMUM_COMMITTEE_SIGNATURE_MEMBER_COUNT } from '../../lib/constants'
import set from 'lodash/set'
import * as styles from './Signatures.css'
import { getCommitteeAnswers, isCommitteeSignature } from '../../lib/utils'
+import { useFormContext } from 'react-hook-form'
type Props = {
applicationId: string
@@ -19,6 +20,8 @@ export const RemoveCommitteeMember = ({
applicationId,
})
+ const { setValue } = useFormContext()
+
const onRemoveMember = () => {
const { currentAnswers, signature } = getCommitteeAnswers(
application.answers,
@@ -36,6 +39,8 @@ export const RemoveCommitteeMember = ({
updatedCommitteeSignature,
)
+ setValue(InputFields.signature.committee, updatedCommitteeSignature)
+
updateApplication(updatedAnswers)
}
}
diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularMember.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularMember.tsx
index bba44b95ff35..3cbf1b88cfac 100644
--- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularMember.tsx
+++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularMember.tsx
@@ -5,6 +5,7 @@ import { MINIMUM_REGULAR_SIGNATURE_MEMBER_COUNT } from '../../lib/constants'
import set from 'lodash/set'
import * as styles from './Signatures.css'
import { getRegularAnswers } from '../../lib/utils'
+import { useFormContext } from 'react-hook-form'
type Props = {
applicationId: string
@@ -21,6 +22,8 @@ export const RemoveRegularMember = ({
applicationId,
})
+ const { setValue } = useFormContext()
+
const onRemoveMember = () => {
const { currentAnswers, signature } = getRegularAnswers(application.answers)
@@ -45,6 +48,8 @@ export const RemoveRegularMember = ({
updatedRegularSignature,
)
+ setValue(InputFields.signature.regular, updatedRegularSignature)
+
updateApplication(updatedAnswers)
}
}
diff --git a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularSignature.tsx b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularSignature.tsx
index 27daca697399..d7734850ca84 100644
--- a/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularSignature.tsx
+++ b/libs/application/templates/official-journal-of-iceland/src/components/signatures/RemoveRegularSignature.tsx
@@ -4,6 +4,7 @@ import { getValueViaPath } from '@island.is/application/core'
import { InputFields } from '../../lib/types'
import { isRegularSignature } from '../../lib/utils'
import set from 'lodash/set'
+import { useFormContext } from 'react-hook-form'
type Props = {
applicationId: string
@@ -18,6 +19,8 @@ export const RemoveRegularSignature = ({
applicationId,
})
+ const { setValue } = useFormContext()
+
const onRemove = () => {
const currentAnswers = structuredClone(application.answers)
const signature = getValueViaPath(
@@ -36,6 +39,8 @@ export const RemoveRegularSignature = ({
updatedRegularSignature,
)
+ setValue(InputFields.signature.regular, updatedRegularSignature)
+
updateApplication(updatedSignatures)
}
}
diff --git a/libs/application/templates/official-journal-of-iceland/src/screens/RequirementsScreen.tsx b/libs/application/templates/official-journal-of-iceland/src/screens/RequirementsScreen.tsx
index d8dfe7783b50..d55719e6588e 100644
--- a/libs/application/templates/official-journal-of-iceland/src/screens/RequirementsScreen.tsx
+++ b/libs/application/templates/official-journal-of-iceland/src/screens/RequirementsScreen.tsx
@@ -14,7 +14,7 @@ import {
DEFAULT_COMMITTEE_SIGNATURE_MEMBER_COUNT,
DEFAULT_REGULAR_SIGNATURE_COUNT,
DEFAULT_REGULAR_SIGNATURE_MEMBER_COUNT,
- OJOI_INPUT_HEIGHT as OJOI_INPUT_HEIGHT,
+ OJOI_INPUT_HEIGHT,
SignatureTypes,
} from '../lib/constants'
import { useApplication } from '../hooks/useUpdateApplication'
diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/assets/ManOnBenchIllustration.tsx b/libs/application/templates/signature-collection/parliamentary-list-signing/assets/ManOnBenchIllustration.tsx
new file mode 100644
index 000000000000..7139eb016608
--- /dev/null
+++ b/libs/application/templates/signature-collection/parliamentary-list-signing/assets/ManOnBenchIllustration.tsx
@@ -0,0 +1,450 @@
+const Man = () => (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+)
+
+export const ManOnBenchIllustration = () => (
+
+)
diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Done.ts b/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Done.ts
index e65e617c9965..7df9cd082957 100644
--- a/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Done.ts
+++ b/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Done.ts
@@ -4,11 +4,14 @@ import {
buildSection,
buildMessageWithLinkButtonField,
buildDescriptionField,
+ buildImageField,
+ buildAlertMessageField,
} from '@island.is/application/core'
import { Application, Form, FormModes } from '@island.is/application/types'
import { m } from '../lib/messages'
import { infer as zinfer } from 'zod'
import { dataSchema } from '../lib/dataSchema'
+import { ManOnBenchIllustration } from '../../assets/ManOnBenchIllustration'
type Answers = zinfer
export const Done: Form = buildForm({
@@ -35,25 +38,37 @@ export const Done: Form = buildForm({
buildMultiField({
id: 'doneScreen',
title: m.listSigned,
- description: (application: Application) => ({
- ...m.listSignedDescription,
- values: {
- name: (application.answers as Answers).list.name,
- },
- }),
children: [
- buildMessageWithLinkButtonField({
- id: 'done.goToServicePortal',
+ buildAlertMessageField({
+ id: 'doneAlertMessage',
title: '',
- url: '/minarsidur/min-gogn/listar/althingis-medmaelasofnun',
- buttonTitle: m.linkFieldButtonTitle,
- message: m.linkFieldMessage,
+ message: (application: Application) => ({
+ ...m.listSignedDescription,
+ values: {
+ name: (application.answers as Answers).list.name,
+ },
+ }),
+ alertType: 'success',
+ }),
+ buildImageField({
+ id: 'doneImage',
+ title: '',
+ image: ManOnBenchIllustration,
+ imageWidth: '50%',
+ imagePosition: 'center',
}),
buildDescriptionField({
id: 'space',
title: '',
space: 'containerGutter',
}),
+ buildMessageWithLinkButtonField({
+ id: 'done.goToServicePortal',
+ title: '',
+ url: '/minarsidur/min-gogn/listar/althingis-medmaelasofnun',
+ buttonTitle: m.linkFieldButtonTitle,
+ message: m.linkFieldMessage,
+ }),
buildDescriptionField({
id: 'space1',
title: '',
diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Draft.ts b/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Draft.ts
index 0ffa0e7feb88..ac12c5c49250 100644
--- a/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Draft.ts
+++ b/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Draft.ts
@@ -1,8 +1,8 @@
import {
buildDescriptionField,
buildForm,
- buildHiddenInput,
buildMultiField,
+ buildRadioField,
buildSection,
buildSubmitField,
buildTextField,
@@ -20,7 +20,7 @@ export const Draft: Form = buildForm({
title: '',
mode: FormModes.DRAFT,
renderLastScreenButton: true,
- renderLastScreenBackButton: true,
+ renderLastScreenBackButton: false,
logo: Logo,
children: [
buildSection({
@@ -33,6 +33,60 @@ export const Draft: Form = buildForm({
title: m.dataCollection,
children: [],
}),
+ /* section used for testing purposes */
+ buildSection({
+ id: 'selectCandidateSection',
+ title: m.selectCandidate,
+ condition: (_, externalData) => {
+ const lists = getValueViaPath(
+ externalData,
+ 'getList.data',
+ [],
+ ) as SignatureCollectionList[]
+ return lists.length > 1
+ },
+ children: [
+ buildMultiField({
+ id: 'selectCandidateSection',
+ title: m.selectCandidate,
+ description: m.selectCandidateDescription,
+ children: [
+ buildRadioField({
+ id: 'listId',
+ title: '',
+ defaultValue: '',
+ required: true,
+ options: ({
+ externalData: {
+ getList: { data },
+ },
+ }) => {
+ return (data as SignatureCollectionList[]).map((list) => ({
+ value: list.id,
+ label: list.candidate.name,
+ disabled:
+ list.maxReached || new Date(list.endTime) < new Date(),
+ tag: list.maxReached
+ ? {
+ label: m.selectCandidateMaxReached.defaultMessage,
+ variant: 'red',
+ outlined: true,
+ }
+ : new Date(list.endTime) < new Date()
+ ? {
+ label: m.selectCandidateListExpired.defaultMessage,
+ variant: 'red',
+ outlined: true,
+ }
+ : undefined,
+ }))
+ },
+ }),
+ ],
+ }),
+ ],
+ }),
+ /* ------------------------------- */
buildSection({
id: 'signListInformationSection',
title: m.information,
@@ -47,24 +101,7 @@ export const Draft: Form = buildForm({
title: m.listHeader,
titleVariant: 'h3',
}),
- buildHiddenInput({
- id: 'listId',
- defaultValue: ({ answers, externalData }: Application) => {
- const lists = getValueViaPath(
- externalData,
- 'getList.data',
- [],
- ) as SignatureCollectionList[]
- const initialQuery = getValueViaPath(
- answers,
- 'initialQuery',
- '',
- )
-
- return lists.find((x) => x.candidate.id === initialQuery)?.id
- },
- }),
buildTextField({
id: 'list.name',
title: m.listName,
@@ -83,7 +120,11 @@ export const Draft: Form = buildForm({
'',
)
- return lists.find((x) => x.candidate.id === initialQuery)?.title
+ return lists.find((list) =>
+ initialQuery
+ ? list.candidate.id === initialQuery
+ : list.id === answers.listId,
+ )?.candidate?.name
},
}),
buildTextField({
@@ -104,8 +145,11 @@ export const Draft: Form = buildForm({
'',
)
- return lists.find((x) => x.candidate.id === initialQuery)
- ?.candidate?.partyBallotLetter
+ return lists.find((list) =>
+ initialQuery
+ ? list.candidate.id === initialQuery
+ : list.id === answers.listId,
+ )?.candidate?.partyBallotLetter
},
}),
buildDescriptionField({
diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Prerequisites.ts b/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Prerequisites.ts
index 88e64f32721f..2455bc31fe78 100644
--- a/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Prerequisites.ts
+++ b/libs/application/templates/signature-collection/parliamentary-list-signing/src/forms/Prerequisites.ts
@@ -87,11 +87,6 @@ export const Prerequisites: Form = buildForm({
title: '',
subTitle: '',
}),
- buildDataProviderItem({
- //provider: TODO: Add providers needed for signing collection,
- title: '',
- subTitle: '',
- }),
],
}),
],
diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/dataSchema.ts b/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/dataSchema.ts
index 212151c18bb1..b7f38ebfe60d 100644
--- a/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/dataSchema.ts
+++ b/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/dataSchema.ts
@@ -3,7 +3,6 @@ import { z } from 'zod'
export const dataSchema = z.object({
/* Gagnaöflun */
approveExternalData: z.boolean().refine((v) => v),
- listId: z.string().min(1),
list: z.object({
name: z.string(),
letter: z.string(),
diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/messages.ts b/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/messages.ts
index 6800b483c490..1c24b6731cef 100644
--- a/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/messages.ts
+++ b/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/messages.ts
@@ -101,6 +101,26 @@ export const m = defineMessages({
defaultMessage: 'Nafn',
description: '',
},
+ selectCandidate: {
+ id: 'pls.application:selectCandidate',
+ defaultMessage: 'Veldu frambjóðanda',
+ description: '',
+ },
+ selectCandidateDescription: {
+ id: 'pls.application:selectCandidateDescription',
+ defaultMessage: 'Frambjóðendur á þínu svæði sem hægt er að mæla með:',
+ description: '',
+ },
+ selectCandidateMaxReached: {
+ id: 'pls.application:selectCandidateMaxReached',
+ defaultMessage: 'Hámarki meðmæla náð',
+ description: '',
+ },
+ selectCandidateListExpired: {
+ id: 'pls.application:selectCandidateListExpired',
+ defaultMessage: 'Söfnuninni lokið',
+ description: '',
+ },
listName: {
id: 'pls.application:listName',
defaultMessage: 'Heiti framboðs',
@@ -186,7 +206,7 @@ export const m = defineMessages({
},
linkFieldMessage: {
id: 'pls.application:linkFieldMessage',
- defaultMessage: 'Á Mínum síðum geturðu séð hvaða framboði þú mældir með',
+ defaultMessage: 'Á Mínum síðum geturðu séð hvaða framboði þú mæltir með',
description: '',
},
diff --git a/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/signListTemplate.ts b/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/signListTemplate.ts
index aba07ec736b7..8c385841626b 100644
--- a/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/signListTemplate.ts
+++ b/libs/application/templates/signature-collection/parliamentary-list-signing/src/lib/signListTemplate.ts
@@ -17,12 +17,7 @@ import { dataSchema } from './dataSchema'
import { m } from './messages'
import { EphemeralStateLifeCycle } from '@island.is/application/core'
import { Features } from '@island.is/feature-flags'
-import {
- CanSignApi,
- CurrentCollectionApi,
- GetListApi,
- OwnerRequirementsApi,
-} from '../dataProviders'
+import { CanSignApi, CurrentCollectionApi, GetListApi } from '../dataProviders'
const WeekLifeCycle: StateLifeCycle = {
shouldBeListed: false,
diff --git a/libs/application/ui-components/src/components/PaymentPending/PaymentPending.tsx b/libs/application/ui-components/src/components/PaymentPending/PaymentPending.tsx
index 376903656e98..d4c040b47b4e 100644
--- a/libs/application/ui-components/src/components/PaymentPending/PaymentPending.tsx
+++ b/libs/application/ui-components/src/components/PaymentPending/PaymentPending.tsx
@@ -5,10 +5,15 @@ import {
DefaultEvents,
FieldBaseProps,
} from '@island.is/application/types'
-import { Box, Button, Text } from '@island.is/island-ui/core'
+import {
+ AlertMessage,
+ Box,
+ Button,
+ LoadingDots,
+ Text,
+} from '@island.is/island-ui/core'
import { useSubmitApplication, usePaymentStatus, useMsg } from './hooks'
import { getRedirectStatus, getRedirectUrl, isComingFromRedirect } from './util'
-import { Company } from './assets'
import { useSearchParams } from 'react-router-dom'
export interface PaymentPendingProps {
@@ -84,10 +89,27 @@ export const PaymentPending: FC<
if (submitError) {
return (
- {msg(coreErrorMessages.paymentSubmitFailed)}
-
+
+
+
+
+
+
)
}
@@ -95,8 +117,17 @@ export const PaymentPending: FC<
return (
{msg(coreMessages.paymentPollingIndicator)}
-
-
+
+
)
diff --git a/libs/auth-api-lib/seeders/20240917153226-set-also-for-delegated-user-false.js b/libs/auth-api-lib/seeders/20240917153226-set-also-for-delegated-user-false.js
index 03879f2dbbee..63d0ef1513bb 100644
--- a/libs/auth-api-lib/seeders/20240917153226-set-also-for-delegated-user-false.js
+++ b/libs/auth-api-lib/seeders/20240917153226-set-also-for-delegated-user-false.js
@@ -4,7 +4,7 @@
BEGIN;
UPDATE api_scope
SET also_for_delegated_user = false
- WHERE name = '@island.is/signature-collection'
+ WHERE name = '@island.is/signature-collection';
COMMIT;
`)
@@ -15,7 +15,7 @@
BEGIN;
UPDATE api_scope
SET also_for_delegated_user = true
- WHERE name = '@island.is/signature-collection'
+ WHERE name = '@island.is/signature-collection';
COMMIT;
`)
diff --git a/libs/auth-api-lib/sequelize.config.js b/libs/auth-api-lib/sequelize.config.js
index 1808cea88c41..de497b35ab6a 100644
--- a/libs/auth-api-lib/sequelize.config.js
+++ b/libs/auth-api-lib/sequelize.config.js
@@ -1,12 +1,12 @@
/* eslint-env node */
module.exports = {
development: {
- username: process.env.DB_USER ?? 'dev_db',
- password: process.env.DB_PASS ?? 'dev_db',
- database: process.env.DB_NAME ?? 'dev_db',
+ username: process.env.DB_USER_AUTH_DB ?? 'dev_db',
+ password: process.env.DB_PASS_AUTH_DB ?? 'dev_db',
+ database: process.env.DB_USER_AUTH_DB ?? 'dev_db',
host: 'localhost',
dialect: 'postgres',
- port: process.env.DB_PORT ?? 5433,
+ port: process.env.DB_PORT_AUTH_DB ?? 5433,
seederStorage: 'sequelize',
},
test: {
diff --git a/libs/auth-api-lib/src/index.ts b/libs/auth-api-lib/src/index.ts
index 68842edcc489..b9305d456101 100644
--- a/libs/auth-api-lib/src/index.ts
+++ b/libs/auth-api-lib/src/index.ts
@@ -39,6 +39,7 @@ export * from './lib/delegations/types/delegationDirection'
export * from './lib/delegations/types/delegationType'
export * from './lib/delegations/types/delegationRecord'
export * from './lib/delegations/types/delegationValidity'
+export * from './lib/delegations/dto/create-paper-delegation.dto'
export * from './lib/delegations/dto/delegation-scope.dto'
export * from './lib/delegations/dto/delegation-admin-custom.dto'
export * from './lib/delegations/dto/delegation.dto'
@@ -58,6 +59,7 @@ export * from './lib/delegations/DelegationConfig'
export * from './lib/delegations/utils/scopes'
export * from './lib/delegations/admin/delegation-admin-custom.service'
export * from './lib/delegations/constants/names'
+export * from './lib/delegations/constants/zendesk'
// Resources module
export * from './lib/resources/resources.module'
diff --git a/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts b/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts
index f5f878a88e15..43ced573b3d8 100644
--- a/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts
+++ b/libs/auth-api-lib/src/lib/delegations/admin/delegation-admin-custom.service.ts
@@ -1,101 +1,211 @@
-import { Injectable } from '@nestjs/common'
+import { BadRequestException, Injectable } from '@nestjs/common'
import { InjectModel } from '@nestjs/sequelize'
+import { Sequelize } from 'sequelize-typescript'
+import kennitala from 'kennitala'
+import { uuid } from 'uuidv4'
+
+import { AuthDelegationType } from '@island.is/shared/types'
+import { User } from '@island.is/auth-nest-tools'
+import { NoContentException } from '@island.is/nest/problem'
+import {
+ Ticket,
+ TicketStatus,
+ ZendeskService,
+} from '@island.is/clients/zendesk'
import { Delegation } from '../models/delegation.model'
import { DelegationAdminCustomDto } from '../dto/delegation-admin-custom.dto'
import { DelegationScope } from '../models/delegation-scope.model'
import { ApiScope } from '../../resources/models/api-scope.model'
import { ApiScopeDelegationType } from '../../resources/models/api-scope-delegation-type.model'
-import { AuthDelegationType } from '@island.is/shared/types'
-import { User } from '@island.is/auth-nest-tools'
import { DelegationResourcesService } from '../../resources/delegation-resources.service'
import { DelegationsIndexService } from '../delegations-index.service'
import { DelegationScopeService } from '../delegation-scope.service'
-import { NoContentException } from '@island.is/nest/problem'
-import { Sequelize } from 'sequelize-typescript'
+import { CreatePaperDelegationDto } from '../dto/create-paper-delegation.dto'
+import { DelegationDTO } from '../dto/delegation.dto'
+import { NamesService } from '../names.service'
+import { DELEGATION_TAG, ZENDESK_CUSTOM_FIELDS } from '../constants/zendesk'
+import { DelegationDelegationType } from '../models/delegation-delegation-type.model'
+import { DelegationsIncomingCustomService } from '../delegations-incoming-custom.service'
+import { DelegationValidity } from '../types/delegationValidity'
@Injectable()
export class DelegationAdminCustomService {
constructor(
@InjectModel(Delegation)
private delegationModel: typeof Delegation,
+ @InjectModel(DelegationDelegationType)
+ private delegationDelegationTypeModel: typeof DelegationDelegationType,
+ private readonly zendeskService: ZendeskService,
private delegationResourceService: DelegationResourcesService,
+ private delegationsIncomingCustomService: DelegationsIncomingCustomService,
private delegationIndexService: DelegationsIndexService,
private delegationScopeService: DelegationScopeService,
+ private namesService: NamesService,
private sequelize: Sequelize,
) {}
+ private getNationalIdsFromZendeskTicket(ticket: Ticket): {
+ fromReferenceId: string
+ toReferenceId: string
+ } {
+ const fromReferenceId = ticket.custom_fields.find(
+ (field) => field.id === ZENDESK_CUSTOM_FIELDS.DelegationFromReferenceId,
+ )
+ const toReferenceId = ticket.custom_fields.find(
+ (field) => field.id === ZENDESK_CUSTOM_FIELDS.DelegationToReferenceId,
+ )
+
+ if (!fromReferenceId || !toReferenceId) {
+ throw new BadRequestException(
+ 'Zendesk ticket is missing required custom fields',
+ )
+ }
+
+ return {
+ fromReferenceId: fromReferenceId.value,
+ toReferenceId: toReferenceId.value,
+ }
+ }
+
async getAllDelegationsByNationalId(
nationalId: string,
): Promise {
- const incomingDelegations = await this.delegationModel.findAll({
- where: {
- toNationalId: nationalId,
- },
- include: [
- {
- model: DelegationScope,
- required: true,
- include: [
- {
- model: ApiScope,
- as: 'apiScope',
- required: true,
- where: {
- enabled: true,
- },
- include: [
- {
- model: ApiScopeDelegationType,
- required: true,
- where: {
- delegationType: AuthDelegationType.Custom,
- },
- },
- ],
- },
- ],
+ const [
+ incomingCustomDelegations,
+ incomingGeneralDelegations,
+ outgoingCustomDelegations,
+ outgoingGeneralDelegations,
+ ] = await Promise.all([
+ this.delegationsIncomingCustomService.findAllValidIncoming({
+ nationalId: nationalId,
+ validity: DelegationValidity.ALL,
+ }),
+ this.delegationsIncomingCustomService.findAllValidGeneralMandate({
+ nationalId: nationalId,
+ }),
+ this.delegationModel.findAll({
+ where: {
+ fromNationalId: nationalId,
},
- ],
- })
-
- const outgoingDelegations = await this.delegationModel.findAll({
- where: {
- fromNationalId: nationalId,
- },
- include: [
- {
- model: DelegationScope,
- required: true,
- include: [
- {
- model: ApiScope,
- required: true,
- as: 'apiScope',
- where: {
- enabled: true,
- },
- include: [
- {
- model: ApiScopeDelegationType,
- required: true,
- where: {
- delegationType: AuthDelegationType.Custom,
- },
+ include: [
+ {
+ model: DelegationScope,
+ required: true,
+ include: [
+ {
+ model: ApiScope,
+ required: true,
+ as: 'apiScope',
+ where: {
+ enabled: true,
},
- ],
- },
- ],
+ include: [
+ {
+ model: ApiScopeDelegationType,
+ required: true,
+ where: {
+ delegationType: AuthDelegationType.Custom,
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ }),
+ this.delegationModel.findAll({
+ where: {
+ fromNationalId: nationalId,
},
- ],
- })
+ include: [
+ {
+ model: DelegationDelegationType,
+ required: true,
+ where: {
+ delegationTypeId: AuthDelegationType.GeneralMandate,
+ },
+ },
+ ],
+ }),
+ ])
return {
- incoming: incomingDelegations.map((delegation) => delegation.toDTO()),
- outgoing: outgoingDelegations.map((delegation) => delegation.toDTO()),
+ incoming: [...incomingCustomDelegations, ...incomingGeneralDelegations],
+ outgoing: [
+ ...outgoingGeneralDelegations.map((d) =>
+ d.toDTO(AuthDelegationType.GeneralMandate),
+ ),
+ ...outgoingCustomDelegations.map((delegation) => delegation.toDTO()),
+ ],
}
}
+ async createDelegation(
+ user: User,
+ delegation: CreatePaperDelegationDto,
+ ): Promise {
+ this.validatePersonsNationalIds(
+ delegation.toNationalId,
+ delegation.fromNationalId,
+ )
+
+ const zendeskCase = await this.zendeskService.getTicket(
+ delegation.referenceId,
+ )
+
+ if (!zendeskCase.tags.includes(DELEGATION_TAG)) {
+ throw new BadRequestException('Zendesk ticket is missing required tag')
+ }
+
+ if (zendeskCase.status !== TicketStatus.Solved) {
+ throw new BadRequestException('Zendesk case is not solved')
+ }
+
+ const { fromReferenceId, toReferenceId } =
+ this.getNationalIdsFromZendeskTicket(zendeskCase)
+
+ if (
+ fromReferenceId !== delegation.fromNationalId ||
+ toReferenceId !== delegation.toNationalId
+ ) {
+ throw new BadRequestException(
+ 'Zendesk ticket nationalIds does not match delegation nationalIds',
+ )
+ }
+
+ const [fromDisplayName, toName] = await Promise.all([
+ this.namesService.getPersonName(delegation.fromNationalId),
+ this.namesService.getPersonName(delegation.toNationalId),
+ ])
+
+ const newDelegation = await this.delegationModel.create(
+ {
+ id: uuid(),
+ toNationalId: delegation.toNationalId,
+ fromNationalId: delegation.fromNationalId,
+ createdByNationalId: user.actor?.nationalId ?? user.nationalId,
+ referenceId: delegation.referenceId,
+ toName,
+ fromDisplayName,
+ delegationDelegationTypes: [
+ {
+ delegationTypeId: AuthDelegationType.GeneralMandate,
+ validTo: delegation.validTo,
+ },
+ ] as DelegationDelegationType[],
+ },
+ {
+ include: [this.delegationDelegationTypeModel],
+ },
+ )
+
+ // Index delegations for the toNationalId
+ void this.indexDelegations(delegation.toNationalId)
+
+ return newDelegation.toDTO(AuthDelegationType.GeneralMandate)
+ }
+
async deleteDelegation(user: User, delegationId: string): Promise {
const delegation = await this.delegationModel.findByPk(delegationId)
@@ -103,6 +213,10 @@ export class DelegationAdminCustomService {
throw new NoContentException()
}
+ if (!delegation.referenceId) {
+ throw new NoContentException()
+ }
+
const userScopes = await this.delegationResourceService.findScopes(
user,
delegation.domainName ?? null,
@@ -119,6 +233,7 @@ export class DelegationAdminCustomService {
const remainingScopes = await this.delegationScopeService.findAll(
delegationId,
)
+
if (remainingScopes.length === 0) {
await this.delegationModel.destroy({
transaction,
@@ -128,10 +243,32 @@ export class DelegationAdminCustomService {
})
}
- // Index custom delegations for the toNationalId
- void this.delegationIndexService.indexCustomDelegations(
- delegation.toNationalId,
- )
+ // Index delegations for the toNationalId
+ void this.indexDelegations(delegation.toNationalId)
})
}
+
+ private validatePersonsNationalIds(
+ toNationalId: string,
+ fromNationalId: string,
+ ) {
+ if (toNationalId === fromNationalId) {
+ throw new BadRequestException(
+ 'Cannot create a delegation between the same nationalId.',
+ )
+ }
+
+ if (
+ !(kennitala.isPerson(fromNationalId) && kennitala.isPerson(toNationalId))
+ ) {
+ throw new BadRequestException(
+ 'National ids needs to be valid person national ids',
+ )
+ }
+ }
+
+ private indexDelegations(nationalId: string) {
+ void this.delegationIndexService.indexCustomDelegations(nationalId)
+ void this.delegationIndexService.indexGeneralMandateDelegations(nationalId)
+ }
}
diff --git a/libs/auth-api-lib/src/lib/delegations/constants/zendesk.ts b/libs/auth-api-lib/src/lib/delegations/constants/zendesk.ts
new file mode 100644
index 000000000000..75b1df95fc5c
--- /dev/null
+++ b/libs/auth-api-lib/src/lib/delegations/constants/zendesk.ts
@@ -0,0 +1,5 @@
+export const DELEGATION_TAG = 'umsokn_um_umboð_a_mínum_síðum'
+export const ZENDESK_CUSTOM_FIELDS = {
+ DelegationToReferenceId: 21401464004498,
+ DelegationFromReferenceId: 21401435545234,
+}
diff --git a/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts b/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts
index bb6f5a332112..94e89cc29204 100644
--- a/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts
+++ b/libs/auth-api-lib/src/lib/delegations/delegation-scope.service.ts
@@ -6,6 +6,8 @@ import startOfDay from 'date-fns/startOfDay'
import { Op, Transaction } from 'sequelize'
import { uuid } from 'uuidv4'
+import { SyslumennService } from '@island.is/clients/syslumenn'
+import { logger } from '@island.is/logging'
import {
AuthDelegationProvider,
AuthDelegationType,
@@ -18,14 +20,14 @@ import { ApiScope } from '../resources/models/api-scope.model'
import { IdentityResource } from '../resources/models/identity-resource.model'
import { DelegationProviderService } from './delegation-provider.service'
import { DelegationConfig } from './DelegationConfig'
+import { DelegationsIndexService } from './delegations-index.service'
import { UpdateDelegationScopeDTO } from './dto/delegation-scope.dto'
+import { DelegationDelegationType } from './models/delegation-delegation-type.model'
import { DelegationScope } from './models/delegation-scope.model'
import { DelegationTypeModel } from './models/delegation-type.model'
import { Delegation } from './models/delegation.model'
import type { User } from '@island.is/auth-nest-tools'
-import { DelegationDelegationType } from './models/delegation-delegation-type.model'
-
@Injectable()
export class DelegationScopeService {
constructor(
@@ -40,6 +42,8 @@ export class DelegationScopeService {
@Inject(DelegationConfig.KEY)
private delegationConfig: ConfigType,
private delegationProviderService: DelegationProviderService,
+ private readonly syslumennService: SyslumennService,
+ private readonly delegationsIndexService: DelegationsIndexService,
) {}
async createOrUpdate(
@@ -304,6 +308,55 @@ export class DelegationScopeService {
return apiScopes.map((s) => s.name)
}
+ private async findDistrictCommissionersRegistryScopesTo(
+ toNationalId: string,
+ fromNationalId: string,
+ ): Promise {
+ // if no valid delegation exists, return empty array
+ try {
+ const delegationFound =
+ await this.syslumennService.checkIfDelegationExists(
+ toNationalId,
+ fromNationalId,
+ )
+
+ if (!delegationFound) {
+ this.delegationsIndexService.removeDelegationRecord({
+ fromNationalId,
+ toNationalId,
+ type: AuthDelegationType.LegalRepresentative,
+ provider: AuthDelegationProvider.DistrictCommissionersRegistry,
+ })
+ return []
+ }
+ } catch (error) {
+ logger.error(
+ `Failed checking if delegation exists at provider '${AuthDelegationProvider.DistrictCommissionersRegistry}'`,
+ )
+ return []
+ }
+
+ // else return all enabled scopes for this provider and provided delegation types
+ const apiScopes = await this.apiScopeModel.findAll({
+ attributes: ['name'],
+ where: {
+ enabled: true,
+ },
+ include: [
+ {
+ model: DelegationTypeModel,
+ required: true,
+ where: {
+ id: AuthDelegationType.LegalRepresentative,
+ provider: AuthDelegationProvider.DistrictCommissionersRegistry,
+ },
+ },
+ ],
+ })
+
+ return apiScopes.map((s) => s.name)
+ }
+
private async findAllAutomaticScopes(): Promise {
const apiScopes = await this.apiScopeModel.findAll({
attributes: ['name'],
@@ -372,6 +425,16 @@ export class DelegationScopeService {
)
}
+ if (
+ providers.includes(AuthDelegationProvider.DistrictCommissionersRegistry)
+ )
+ scopePromises.push(
+ this.findDistrictCommissionersRegistryScopesTo(
+ user.nationalId,
+ fromNationalId,
+ ),
+ )
+
const scopeSets = await Promise.all(scopePromises)
let scopes = ([] as string[]).concat(...scopeSets)
diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts
index b13ce486becc..4e07372ce897 100644
--- a/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts
+++ b/libs/auth-api-lib/src/lib/delegations/delegations-incoming-custom.service.ts
@@ -33,6 +33,7 @@ import { DelegationDelegationType } from './models/delegation-delegation-type.mo
type FindAllValidIncomingOptions = {
nationalId: string
domainName?: string
+ validity?: DelegationValidity
}
type FromNameInfo = {
@@ -59,13 +60,17 @@ export class DelegationsIncomingCustomService {
) {}
async findAllValidIncoming(
- { nationalId, domainName }: FindAllValidIncomingOptions,
+ {
+ nationalId,
+ domainName,
+ validity = DelegationValidity.NOW,
+ }: FindAllValidIncomingOptions,
useMaster = false,
): Promise {
const { delegations, fromNameInfo } = await this.findAllIncoming(
{
nationalId,
- validity: DelegationValidity.NOW,
+ validity,
domainName,
},
useMaster,
diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts
index 30eb53b694c3..56355428a63e 100644
--- a/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts
+++ b/libs/auth-api-lib/src/lib/delegations/delegations-incoming.service.ts
@@ -1,4 +1,4 @@
-import { BadRequestException, Inject, Injectable } from '@nestjs/common'
+import { BadRequestException, Injectable } from '@nestjs/common'
import { InjectModel } from '@nestjs/sequelize'
import { User } from '@island.is/auth-nest-tools'
@@ -6,7 +6,8 @@ import {
IndividualDto,
NationalRegistryClientService,
} from '@island.is/clients/national-registry-v2'
-import { type Logger, LOGGER_PROVIDER } from '@island.is/logging'
+import { SyslumennService } from '@island.is/clients/syslumenn'
+import { logger } from '@island.is/logging'
import { FeatureFlagService, Features } from '@island.is/nest/feature-flags'
import {
AuthDelegationProvider,
@@ -53,8 +54,6 @@ interface FindAvailableInput {
@Injectable()
export class DelegationsIncomingService {
constructor(
- @Inject(LOGGER_PROVIDER)
- protected readonly logger: Logger,
@InjectModel(Client)
private clientModel: typeof Client,
@InjectModel(ClientAllowedScope)
@@ -69,6 +68,7 @@ export class DelegationsIncomingService {
private delegationProviderService: DelegationProviderService,
private nationalRegistryClient: NationalRegistryClientService,
private readonly featureFlagService: FeatureFlagService,
+ private readonly syslumennService: SyslumennService,
) {}
async findAllValid(
@@ -272,6 +272,45 @@ export class DelegationsIncomingService {
return [...mergedDelegationMap.values()]
}
+ async verifyDelegationAtProvider(
+ user: User,
+ fromNationalId: string,
+ delegationTypes: AuthDelegationType[],
+ ): Promise {
+ const providers = await this.delegationProviderService.findProviders(
+ delegationTypes,
+ )
+
+ if (
+ providers.includes(AuthDelegationProvider.DistrictCommissionersRegistry)
+ ) {
+ try {
+ const delegationFound =
+ await this.syslumennService.checkIfDelegationExists(
+ user.nationalId,
+ fromNationalId,
+ )
+
+ if (delegationFound) {
+ return true
+ } else {
+ this.delegationsIndexService.removeDelegationRecord({
+ fromNationalId,
+ toNationalId: user.nationalId,
+ type: AuthDelegationType.LegalRepresentative,
+ provider: AuthDelegationProvider.DistrictCommissionersRegistry,
+ })
+ }
+ } catch (error) {
+ logger.error(
+ `Failed checking if delegation exists at provider '${AuthDelegationProvider.DistrictCommissionersRegistry}'`,
+ )
+ }
+ }
+
+ return false
+ }
+
private async getAvailableDistrictCommissionersRegistryDelegations(
user: User,
types: AuthDelegationType[],
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 fb382c183b49..46b51cd42f48 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
@@ -42,6 +42,17 @@ import {
const TEN_MINUTES = 1000 * 60 * 10
const ONE_WEEK = 1000 * 60 * 60 * 24 * 7
+// When delegation providers have been refactored to use the webhook method
+// with hard check on action we need to exclude them from the standard indexing.
+// We register our current providers as indexed, as all new providers are expected
+// to use the webhook method.
+const INDEXED_DELEGATION_PROVIDERS = [
+ AuthDelegationProvider.Custom,
+ AuthDelegationProvider.PersonalRepresentativeRegistry,
+ AuthDelegationProvider.CompanyRegistry,
+ AuthDelegationProvider.NationalRegistry,
+]
+
export type DelegationIndexInfo = Pick<
DelegationIndex,
| 'toNationalId'
@@ -269,6 +280,12 @@ export class DelegationsIndexService {
await this.saveToIndex(nationalId, delegations)
}
+ /* Index incoming general mandate delegations */
+ async indexGeneralMandateDelegations(nationalId: string) {
+ const delegations = await this.getGeneralMandateDelegation(nationalId, true)
+ await this.saveToIndex(nationalId, delegations)
+ }
+
/* Index incoming personal representative delegations */
async indexRepresentativeDelegations(nationalId: string) {
const delegations = await this.getRepresentativeDelegations(
@@ -343,6 +360,7 @@ export class DelegationsIndexService {
const currRecords = await this.delegationIndexModel.findAll({
where: {
toNationalId: nationalId,
+ provider: INDEXED_DELEGATION_PROVIDERS,
},
})
@@ -471,6 +489,19 @@ export class DelegationsIndexService {
)
}
+ private async getGeneralMandateDelegation(
+ nationalId: string,
+ useMaster = false,
+ ) {
+ const delegation =
+ await this.delegationsIncomingCustomService.findAllValidGeneralMandate(
+ { nationalId },
+ useMaster,
+ )
+
+ return delegation.map(toDelegationIndexInfo)
+ }
+
private async getCustomDelegations(nationalId: string, useMaster = false) {
const delegations =
await this.delegationsIncomingCustomService.findAllValidIncoming(
diff --git a/libs/auth-api-lib/src/lib/delegations/delegations-outgoing.service.ts b/libs/auth-api-lib/src/lib/delegations/delegations-outgoing.service.ts
index ab615439e5fb..807298a7fa2a 100644
--- a/libs/auth-api-lib/src/lib/delegations/delegations-outgoing.service.ts
+++ b/libs/auth-api-lib/src/lib/delegations/delegations-outgoing.service.ts
@@ -8,6 +8,7 @@ import {
import { InjectModel } from '@nestjs/sequelize'
import { and, Op, WhereOptions } from 'sequelize'
import { isUuid, uuid } from 'uuidv4'
+import startOfDay from 'date-fns/startOfDay'
import { User } from '@island.is/auth-nest-tools'
import { NoContentException } from '@island.is/nest/problem'
@@ -39,6 +40,8 @@ import {
import { Features } from '@island.is/feature-flags'
import { FeatureFlagService } from '@island.is/nest/feature-flags'
import { LOGGER_PROVIDER } from '@island.is/logging'
+import { DelegationDelegationType } from './models/delegation-delegation-type.model'
+import { AuthDelegationType } from '@island.is/shared/types'
/**
* Service class for outgoing delegations.
@@ -68,42 +71,71 @@ export class DelegationsOutgoingService {
if (otherUser) {
return this.findByOtherUser(user, otherUser, domainName)
}
- const delegations = await this.delegationModel.findAll({
- where: and(
- {
+
+ const [delegations, delegationTypesDelegations] = await Promise.all([
+ this.delegationModel.findAll({
+ where: and(
+ {
+ fromNationalId: user.nationalId,
+ },
+ domainName ? { domainName } : {},
+ getDelegationNoActorWhereClause(user),
+ ...(await this.delegationResourceService.apiScopeFilter({
+ user,
+ prefix: 'delegationScopes->apiScope',
+ direction: DelegationDirection.OUTGOING,
+ })),
+ ),
+ include: [
+ {
+ model: DelegationScope,
+ include: [
+ {
+ attributes: ['displayName'],
+ model: ApiScope,
+ required: true,
+ include: [
+ ...this.delegationResourceService.apiScopeInclude(
+ user,
+ DelegationDirection.OUTGOING,
+ ),
+ ],
+ },
+ ],
+ required: validity !== DelegationValidity.ALL,
+ where: getScopeValidityWhereClause(validity),
+ },
+ ],
+ }),
+ this.delegationModel.findAll({
+ where: {
fromNationalId: user.nationalId,
},
- domainName ? { domainName } : {},
- getDelegationNoActorWhereClause(user),
- ...(await this.delegationResourceService.apiScopeFilter({
- user,
- prefix: 'delegationScopes->apiScope',
- direction: DelegationDirection.OUTGOING,
- })),
- ),
- include: [
- {
- model: DelegationScope,
- include: [
- {
- attributes: ['displayName'],
- model: ApiScope,
- required: true,
- include: [
- ...this.delegationResourceService.apiScopeInclude(
- user,
- DelegationDirection.OUTGOING,
- ),
- ],
+ include: [
+ {
+ model: DelegationDelegationType,
+ where: {
+ delegationTypeId: AuthDelegationType.GeneralMandate,
+ validTo: {
+ [Op.or]: {
+ [Op.gte]: startOfDay(new Date()),
+ [Op.is]: null,
+ },
+ },
},
- ],
- required: validity !== DelegationValidity.ALL,
- where: getScopeValidityWhereClause(validity),
- },
- ],
- })
+ required: true,
+ },
+ ],
+ }),
+ ])
+
+ const delegationTypesDTO = delegationTypesDelegations.map((d) =>
+ d.toDTO(AuthDelegationType.GeneralMandate),
+ )
+
+ const delegationsDTO = delegations.map((d) => d.toDTO())
- return delegations.map((d) => d.toDTO())
+ return [...delegationsDTO, ...delegationTypesDTO]
}
async findByOtherUser(
diff --git a/libs/auth-api-lib/src/lib/delegations/delegations.module.ts b/libs/auth-api-lib/src/lib/delegations/delegations.module.ts
index c74aaca8a940..1fb0530b43d0 100644
--- a/libs/auth-api-lib/src/lib/delegations/delegations.module.ts
+++ b/libs/auth-api-lib/src/lib/delegations/delegations.module.ts
@@ -1,40 +1,46 @@
import { Module } from '@nestjs/common'
import { SequelizeModule } from '@nestjs/sequelize'
-import { NationalRegistryClientModule } from '@island.is/clients/national-registry-v2'
import { RskRelationshipsClientModule } from '@island.is/clients-rsk-relationships'
+import { NationalRegistryClientModule } from '@island.is/clients/national-registry-v2'
import { CompanyRegistryClientModule } from '@island.is/clients/rsk/company-registry'
+import { SyslumennClientModule } from '@island.is/clients/syslumenn'
import { FeatureFlagModule } from '@island.is/nest/feature-flags'
-import { UserSystemNotificationModule } from '../user-notification'
+import {
+ ZendeskModule,
+ ZendeskServiceOptions,
+} from '@island.is/clients/zendesk'
import { ClientAllowedScope } from '../clients/models/client-allowed-scope.model'
import { Client } from '../clients/models/client.model'
import { PersonalRepresentativeModule } from '../personal-representative/personal-representative.module'
+import { ApiScopeDelegationType } from '../resources/models/api-scope-delegation-type.model'
+import { ApiScopeUserAccess } from '../resources/models/api-scope-user-access.model'
import { ApiScope } from '../resources/models/api-scope.model'
import { IdentityResource } from '../resources/models/identity-resource.model'
import { ResourcesModule } from '../resources/resources.module'
+import { UserIdentitiesModule } from '../user-identities/user-identities.module'
+import { UserSystemNotificationModule } from '../user-notification'
+import { DelegationAdminCustomService } from './admin/delegation-admin-custom.service'
+import { DelegationProviderService } from './delegation-provider.service'
import { DelegationScopeService } from './delegation-scope.service'
-import { DelegationsOutgoingService } from './delegations-outgoing.service'
-import { DelegationsService } from './delegations.service'
-import { DelegationsIncomingService } from './delegations-incoming.service'
-import { DelegationScope } from './models/delegation-scope.model'
-import { Delegation } from './models/delegation.model'
-import { NamesService } from './names.service'
-import { DelegationsIncomingWardService } from './delegations-incoming-ward.service'
import { IncomingDelegationsCompanyService } from './delegations-incoming-company.service'
import { DelegationsIncomingCustomService } from './delegations-incoming-custom.service'
import { DelegationsIncomingRepresentativeService } from './delegations-incoming-representative.service'
-import { ApiScopeUserAccess } from '../resources/models/api-scope-user-access.model'
-import { DelegationIndex } from './models/delegation-index.model'
-import { DelegationIndexMeta } from './models/delegation-index-meta.model'
+import { DelegationsIncomingWardService } from './delegations-incoming-ward.service'
+import { DelegationsIncomingService } from './delegations-incoming.service'
import { DelegationsIndexService } from './delegations-index.service'
-import { UserIdentitiesModule } from '../user-identities/user-identities.module'
-import { DelegationTypeModel } from './models/delegation-type.model'
-import { DelegationProviderModel } from './models/delegation-provider.model'
-import { DelegationProviderService } from './delegation-provider.service'
-import { ApiScopeDelegationType } from '../resources/models/api-scope-delegation-type.model'
-import { DelegationAdminCustomService } from './admin/delegation-admin-custom.service'
+import { DelegationsOutgoingService } from './delegations-outgoing.service'
+import { DelegationsService } from './delegations.service'
+import { environment } from '../environments'
import { DelegationDelegationType } from './models/delegation-delegation-type.model'
+import { DelegationIndexMeta } from './models/delegation-index-meta.model'
+import { DelegationIndex } from './models/delegation-index.model'
+import { DelegationProviderModel } from './models/delegation-provider.model'
+import { DelegationScope } from './models/delegation-scope.model'
+import { DelegationTypeModel } from './models/delegation-type.model'
+import { Delegation } from './models/delegation.model'
+import { NamesService } from './names.service'
@Module({
imports: [
@@ -45,6 +51,7 @@ import { DelegationDelegationType } from './models/delegation-delegation-type.mo
CompanyRegistryClientModule,
UserIdentitiesModule,
FeatureFlagModule,
+ ZendeskModule.register(environment.zendeskOptions as ZendeskServiceOptions),
SequelizeModule.forFeature([
ApiScope,
ApiScopeDelegationType,
@@ -61,6 +68,7 @@ import { DelegationDelegationType } from './models/delegation-delegation-type.mo
DelegationDelegationType,
]),
UserSystemNotificationModule,
+ SyslumennClientModule,
],
providers: [
DelegationsService,
diff --git a/libs/auth-api-lib/src/lib/delegations/dto/create-paper-delegation.dto.ts b/libs/auth-api-lib/src/lib/delegations/dto/create-paper-delegation.dto.ts
new file mode 100644
index 000000000000..addcc91b66b5
--- /dev/null
+++ b/libs/auth-api-lib/src/lib/delegations/dto/create-paper-delegation.dto.ts
@@ -0,0 +1,21 @@
+import { IsDateString, IsOptional, IsString } from 'class-validator'
+import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
+
+export class CreatePaperDelegationDto {
+ @IsString()
+ @ApiProperty()
+ fromNationalId!: string
+
+ @IsString()
+ @ApiProperty()
+ toNationalId!: string
+
+ @ApiProperty()
+ @IsString()
+ referenceId!: string
+
+ @IsOptional()
+ @IsDateString()
+ @ApiPropertyOptional({ nullable: true, type: Date })
+ validTo?: Date | null
+}
diff --git a/libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts b/libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts
index ba00c7fc4b3f..f84933c7a80a 100644
--- a/libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts
+++ b/libs/auth-api-lib/src/lib/delegations/models/delegation-delegation-type.model.ts
@@ -51,7 +51,7 @@ export class DelegationDelegationType extends Model<
type: DataType.DATE,
allowNull: true,
})
- validTo?: Date
+ validTo?: Date | null
@CreatedAt
readonly created!: CreationOptional
diff --git a/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts b/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts
index 41ef58402d08..cff697005338 100644
--- a/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts
+++ b/libs/auth-api-lib/src/lib/delegations/models/delegation.model.ts
@@ -25,6 +25,7 @@ import {
AuthDelegationType,
} from '@island.is/shared/types'
import { DelegationDelegationType } from './delegation-delegation-type.model'
+import { isDefined } from '@island.is/shared/utils'
@Table({
tableName: 'delegation',
@@ -96,6 +97,16 @@ export class Delegation extends Model<
referenceId?: string
get validTo(): Date | null | undefined {
+ if (
+ this.delegationDelegationTypes &&
+ this.delegationDelegationTypes.length > 0
+ ) {
+ const dates = this.delegationDelegationTypes
+ .map((x) => x.validTo)
+ .filter((x) => isDefined(x)) as Array
+ return max(dates)
+ }
+
// 1. Find a value with null as validTo. Null means that delegation scope set valid not to a specific time period
const withNullValue = this.delegationScopes?.find((x) => x.validTo === null)
if (withNullValue) {
@@ -104,7 +115,7 @@ export class Delegation extends Model<
// 2. Find items with value in the array
const dates = (this.delegationScopes
- ?.filter((x) => x.validTo !== null && x.validTo !== undefined)
+ ?.filter((x) => isDefined(x.validTo))
.map((x) => x.validTo) || []) as Array
// Return the max value
@@ -136,6 +147,7 @@ export class Delegation extends Model<
: [],
provider: AuthDelegationProvider.Custom,
type: type,
+ referenceId: this.referenceId,
domainName: this.domainName,
}
}
diff --git a/libs/auth-api-lib/src/lib/environments/environment.ts b/libs/auth-api-lib/src/lib/environments/environment.ts
new file mode 100644
index 000000000000..9db41f8ead3b
--- /dev/null
+++ b/libs/auth-api-lib/src/lib/environments/environment.ts
@@ -0,0 +1,9 @@
+const config = {
+ zendeskOptions: {
+ email: process.env.ZENDESK_CONTACT_FORM_EMAIL,
+ token: process.env.ZENDESK_CONTACT_FORM_TOKEN,
+ subdomain: process.env.ZENDESK_CONTACT_FORM_SUBDOMAIN,
+ },
+}
+
+export default config
diff --git a/libs/auth-api-lib/src/lib/environments/index.ts b/libs/auth-api-lib/src/lib/environments/index.ts
new file mode 100644
index 000000000000..f1c9690a5bd4
--- /dev/null
+++ b/libs/auth-api-lib/src/lib/environments/index.ts
@@ -0,0 +1 @@
+export { default as environment } from './environment'
diff --git a/libs/clients/health-directorate/src/lib/clients/organ-donation/clientConfig.json b/libs/clients/health-directorate/src/lib/clients/organ-donation/clientConfig.json
index 3f2b4e84bf09..b83056a0484c 100644
--- a/libs/clients/health-directorate/src/lib/clients/organ-donation/clientConfig.json
+++ b/libs/clients/health-directorate/src/lib/clients/organ-donation/clientConfig.json
@@ -75,6 +75,13 @@
"in": "query",
"description": "The IP address of the user",
"schema": { "type": "string" }
+ },
+ {
+ "name": "locale",
+ "required": false,
+ "in": "query",
+ "description": "The locale to use for the response",
+ "schema": { "$ref": "#/components/schemas/Locale" }
}
],
"requestBody": {
diff --git a/libs/clients/health-directorate/src/lib/clients/organ-donation/organDonation.service.ts b/libs/clients/health-directorate/src/lib/clients/organ-donation/organDonation.service.ts
index 37ceeb3da061..e1b0b502a496 100644
--- a/libs/clients/health-directorate/src/lib/clients/organ-donation/organDonation.service.ts
+++ b/libs/clients/health-directorate/src/lib/clients/organ-donation/organDonation.service.ts
@@ -46,11 +46,13 @@ export class HealthDirectorateOrganDonationService {
public async updateOrganDonation(
auth: Auth,
input: UpdateOrganDonorDto,
+ locale: Locale,
): Promise {
await this.organDonationApiWithAuth(
auth,
).meDonorStatusControllerUpdateOrganDonorStatus({
updateOrganDonorDto: input,
+ locale: locale,
})
}
diff --git a/libs/clients/signature-collection/src/clientConfig.json b/libs/clients/signature-collection/src/clientConfig.json
index edf7e250b0cd..4928aa94d947 100644
--- a/libs/clients/signature-collection/src/clientConfig.json
+++ b/libs/clients/signature-collection/src/clientConfig.json
@@ -74,6 +74,44 @@
}
}
},
+ "/Admin/Medmaeli/{ID}/UpdateBls": {
+ "patch": {
+ "tags": ["Admin"],
+ "summary": "Uppfærir blaðsíðunúmer skriflegs meðmælis",
+ "description": "Aðeins m0gulegt fyrir skrifleg meðmæli",
+ "parameters": [
+ {
+ "name": "ID",
+ "in": "path",
+ "description": "ID meðmælis sem á að uppfæra",
+ "required": true,
+ "schema": { "type": "integer", "format": "int32" }
+ },
+ {
+ "name": "blsNr",
+ "in": "query",
+ "description": "Nýtt blaðsíðutal",
+ "schema": { "type": "integer", "format": "int32" }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/MedmaeliBaseDTO" }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": { "schema": { "type": "string" } }
+ }
+ }
+ }
+ }
+ },
"/Admin/Medmaelalisti/{ID}": {
"delete": {
"tags": ["Admin"],
@@ -1722,6 +1760,44 @@
}
}
},
+ "/Medmaeli/{ID}/UpdateBls": {
+ "patch": {
+ "tags": ["Medmaeli"],
+ "summary": "Uppfærir blaðsíðunúmer skriflegs meðmælis",
+ "description": "Aðeins m0gulegt fyrir skrifleg meðmæli",
+ "parameters": [
+ {
+ "name": "ID",
+ "in": "path",
+ "description": "ID meðmælis sem á að uppfæra",
+ "required": true,
+ "schema": { "type": "integer", "format": "int32" }
+ },
+ {
+ "name": "blsNr",
+ "in": "query",
+ "description": "Nýtt blaðsíðutal",
+ "schema": { "type": "integer", "format": "int32" }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "content": {
+ "application/json": {
+ "schema": { "$ref": "#/components/schemas/MedmaeliBaseDTO" }
+ }
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "content": {
+ "application/json": { "schema": { "type": "string" } }
+ }
+ }
+ }
+ }
+ },
"/Tegund/Kosning": {
"get": {
"tags": ["Tegund"],
@@ -2202,11 +2278,12 @@
"additionalProperties": false
},
"SvaediDTO": {
- "required": ["nafn", "svaediTegundLysing"],
+ "required": ["nafn", "nr", "svaediTegundLysing"],
"type": "object",
"properties": {
"id": { "type": "integer", "format": "int32" },
"nafn": { "type": "string" },
+ "nr": { "type": "string" },
"svaediTegund": { "type": "integer", "format": "int32" },
"svaediTegundLysing": { "type": "string" },
"fjoldi": { "type": "integer", "format": "int32" },
diff --git a/libs/clients/signature-collection/src/lib/signature-collection-admin.service.ts b/libs/clients/signature-collection/src/lib/signature-collection-admin.service.ts
index d692dc89a01e..d6ba1183faf0 100644
--- a/libs/clients/signature-collection/src/lib/signature-collection-admin.service.ts
+++ b/libs/clients/signature-collection/src/lib/signature-collection-admin.service.ts
@@ -293,4 +293,23 @@ export class SignatureCollectionAdminClientService {
return { success: false, reasons: [ReasonKey.DeniedByService] }
}
}
+
+ async updateSignaturePageNumber(
+ auth: Auth,
+ signatureId: string,
+ pageNumber: number,
+ ): Promise {
+ try {
+ const res = await this.getApiWithAuth(
+ this.signatureApi,
+ auth,
+ ).medmaeliIDUpdateBlsPatch({
+ iD: parseInt(signatureId),
+ blsNr: pageNumber,
+ })
+ return { success: res.bladsidaNr === pageNumber }
+ } catch {
+ return { success: false }
+ }
+ }
}
diff --git a/libs/clients/signature-collection/src/lib/signature-collection.service.spec.ts b/libs/clients/signature-collection/src/lib/signature-collection.service.spec.ts
index 30ffd86f9c2d..22fb2aaa1b9f 100644
--- a/libs/clients/signature-collection/src/lib/signature-collection.service.spec.ts
+++ b/libs/clients/signature-collection/src/lib/signature-collection.service.spec.ts
@@ -26,7 +26,7 @@ const sofnun: MedmaelasofnunExtendedDTO[] = [
id: 123,
sofnunStart: new Date('01.01.1900'),
sofnunEnd: new Date('01.01.2199'),
- svaedi: [{ id: 123, nafn: 'Svæði', svaediTegundLysing: 'Lýsing' }],
+ svaedi: [{ id: 123, nafn: 'Svæði', svaediTegundLysing: 'Lýsing', nr: '1' }],
frambodList: [{ id: 123, kennitala: '0101010119', nafn: 'Jónsframboð' }],
kosning: {
id: 123,
@@ -40,7 +40,7 @@ const sofnun: MedmaelasofnunExtendedDTO[] = [
},
]
const sofnunUser: EinstaklingurKosningInfoDTO = {
- svaedi: { id: 123, nafn: 'Svæði', svaediTegundLysing: 'Lýsing' },
+ svaedi: { id: 123, nafn: 'Svæði', svaediTegundLysing: 'Lýsing', nr: '1' },
kennitala: '0101302399',
maFrambod: true,
maFrambodInfo: { aldur: true, rikisfang: true, kennitala: '0101302399' },
@@ -143,7 +143,12 @@ describe('MyService', () => {
kosningTegund: 'Forsetakosning',
},
frambod: { id: 123, kennitala: '0101016789', nafn: 'Jónsframboð' },
- svaedi: { id: 123, nafn: 'Svæði', svaediTegundLysing: 'Lýsing' },
+ svaedi: {
+ id: 123,
+ nafn: 'Svæði',
+ svaediTegundLysing: 'Lýsing',
+ nr: '1',
+ },
dagsetningLokar: new Date('01.01.2199'),
listaLokad: false,
frambodNafn: 'Jónsframboð',
@@ -158,7 +163,12 @@ describe('MyService', () => {
kosningTegund: 'Forsetakosning',
},
frambod: { id: 321, kennitala: '0202026789', nafn: 'Jónsframboð' },
- svaedi: { id: 321, nafn: 'Svæði', svaediTegundLysing: 'Lýsing' },
+ svaedi: {
+ id: 321,
+ nafn: 'Svæði',
+ svaediTegundLysing: 'Lýsing',
+ nr: '1',
+ },
dagsetningLokar: new Date('01.01.1900'),
listaLokad: true,
frambodNafn: 'Jónsframboð',
@@ -243,7 +253,12 @@ describe('MyService', () => {
sofnunEnd: new Date('01.01.2199'),
},
frambod: { id: 123, kennitala: '0101016789', nafn: 'Jónsframboð' },
- svaedi: { id: 123, nafn: 'Svæði', svaediTegundLysing: 'Lýsing' },
+ svaedi: {
+ id: 123,
+ nafn: 'Svæði',
+ svaediTegundLysing: 'Lýsing',
+ nr: '1',
+ },
dagsetningLokar: new Date('01.01.2199'),
listaLokad: false,
frambodNafn: 'Jónsframboð',
diff --git a/libs/clients/signature-collection/src/lib/signature-collection.service.ts b/libs/clients/signature-collection/src/lib/signature-collection.service.ts
index d9df57925283..0a99dc8bd331 100644
--- a/libs/clients/signature-collection/src/lib/signature-collection.service.ts
+++ b/libs/clients/signature-collection/src/lib/signature-collection.service.ts
@@ -297,8 +297,11 @@ export class SignatureCollectionClientService {
}
async unsignList(listId: string, auth: User): Promise {
+ const { isPresidential } = await this.currentCollection()
const { signatures } = await this.getSignee(auth)
- const activeSignature = signatures?.find((signature) => signature.valid)
+ const activeSignature = signatures?.find((signature) =>
+ isPresidential ? signature.valid : signature.listId === listId,
+ )
if (!signatures || !activeSignature || activeSignature.listId !== listId) {
return { success: false, reasons: [ReasonKey.SignatureNotFound] }
}
@@ -358,7 +361,7 @@ export class SignatureCollectionClientService {
async getSignedList(auth: User): Promise {
const { signatures } = await this.getSignee(auth)
- const { endTime } = await this.currentCollection()
+ const { endTime, isPresidential } = await this.currentCollection()
if (!signatures) {
return null
}
@@ -372,14 +375,16 @@ export class SignatureCollectionClientService {
)
const isExtended = list.endTime > endTime
const signedThisPeriod = signature.isInitialType === !isExtended
+ const canUnsignDigital = isPresidential ? signature.isDigital : true
+ const canUnsignInvalid = isPresidential ? signature.valid : true
return {
signedDate: signature.created,
isDigital: signature.isDigital,
pageNumber: signature.pageNumber,
isValid: signature.valid,
canUnsign:
- signature.isDigital &&
- signature.valid &&
+ canUnsignDigital &&
+ canUnsignInvalid &&
list.active &&
signedThisPeriod,
...list,
@@ -523,4 +528,23 @@ export class SignatureCollectionClientService {
}
return { success: true }
}
+
+ async getCollectors(
+ auth: User,
+ candidateId: string,
+ ): Promise<{ name: string; nationalId: string }[]> {
+ const candidate = await this.getApiWithAuth(
+ this.candidateApi,
+ auth,
+ ).frambodIDGet({
+ iD: parseInt(candidateId),
+ })
+
+ return (
+ candidate.umbodList?.map((u) => ({
+ name: u.nafn ?? '',
+ nationalId: u.kennitala ?? '',
+ })) ?? []
+ )
+ }
}
diff --git a/libs/clients/syslumenn/src/clientConfig.json b/libs/clients/syslumenn/src/clientConfig.json
index 22ef380cfc45..3c6ac47e291a 100644
--- a/libs/clients/syslumenn/src/clientConfig.json
+++ b/libs/clients/syslumenn/src/clientConfig.json
@@ -472,6 +472,58 @@
]
}
},
+ "/api/Logradamadur/{audkenni}": {
+ "get": {
+ "tags": ["Syslumenn"],
+ "operationId": "Logradamadur_Get",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "audkenni",
+ "in": "path",
+ "required": true,
+ "x-nullable": false
+ },
+ {
+ "type": "string",
+ "name": "kennitala",
+ "in": "query",
+ "x-nullable": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "x-nullable": false,
+ "description": "Success",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/LogradamadurSvar"
+ }
+ }
+ },
+ "500": {
+ "x-nullable": false,
+ "description": "Internal server error",
+ "schema": {
+ "$ref": "#/definitions/ProblemDetails"
+ }
+ },
+ "401": {
+ "x-nullable": false,
+ "description": "Unauthorized",
+ "schema": {
+ "$ref": "#/definitions/ProblemDetails"
+ }
+ }
+ },
+ "security": [
+ {
+ "JWT Token": []
+ }
+ ]
+ }
+ },
"/api/Logmannalisti": {
"get": {
"tags": ["Syslumenn"],
@@ -862,6 +914,99 @@
]
}
},
+ "/v1/Heimagistingar/{audkenni}": {
+ "put": {
+ "tags": ["Syslumenn"],
+ "operationId": "Heimagistingar_Put",
+ "consumes": ["application/json", "text/json", "application/*+json"],
+ "parameters": [
+ {
+ "name": "payload",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/UppfaeraHeimagistingarModel"
+ },
+ "x-nullable": false
+ },
+ {
+ "type": "string",
+ "name": "audkenni",
+ "in": "path",
+ "required": true,
+ "x-nullable": false
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": ""
+ },
+ "401": {
+ "x-nullable": false,
+ "description": "",
+ "schema": {
+ "$ref": "#/definitions/ProblemDetails"
+ }
+ },
+ "default": {
+ "x-nullable": false,
+ "description": "",
+ "schema": {
+ "$ref": "#/definitions/ProblemDetails"
+ }
+ }
+ },
+ "security": [
+ {
+ "JWT Token": []
+ }
+ ]
+ },
+ "get": {
+ "tags": ["Syslumenn"],
+ "operationId": "Heimagistingar_Get",
+ "parameters": [
+ {
+ "type": "string",
+ "name": "audkenni",
+ "in": "path",
+ "required": true,
+ "x-nullable": false
+ }
+ ],
+ "responses": {
+ "200": {
+ "x-nullable": false,
+ "description": "",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/HeimagistingarModel"
+ }
+ }
+ },
+ "401": {
+ "x-nullable": false,
+ "description": "",
+ "schema": {
+ "$ref": "#/definitions/ProblemDetails"
+ }
+ },
+ "default": {
+ "x-nullable": false,
+ "description": "",
+ "schema": {
+ "$ref": "#/definitions/ProblemDetails"
+ }
+ }
+ },
+ "security": [
+ {
+ "JWT Token": []
+ }
+ ]
+ }
+ },
"/v1/VirkarHeimagistingar/{audkenni}": {
"get": {
"tags": ["Syslumenn"],
@@ -1154,13 +1299,7 @@
},
{
"type": "string",
- "name": "Hjonaefni1",
- "in": "query",
- "x-nullable": true
- },
- {
- "type": "string",
- "name": "Hjonaefni2",
+ "name": "kennitala",
"in": "query",
"x-nullable": true
}
@@ -1356,7 +1495,7 @@
"type": "string",
"name": "malsnumer",
"in": "query",
- "x-nullable": false
+ "x-nullable": true
},
{
"type": "string",
@@ -3356,6 +3495,18 @@
}
}
},
+ "LogradamadurSvar": {
+ "type": "object",
+ "properties": {
+ "kennitala": {
+ "type": "string"
+ },
+ "gildirTil": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
"Logmenn": {
"type": "object",
"properties": {
@@ -3672,6 +3823,62 @@
}
}
},
+ "UppfaeraHeimagistingarModel": {
+ "type": "object",
+ "properties": {
+ "leyfi": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/UppfaeraHeimagistingDetail"
+ }
+ }
+ }
+ },
+ "UppfaeraHeimagistingDetail": {
+ "type": "object",
+ "properties": {
+ "tegund": {
+ "type": "string"
+ },
+ "numer": {
+ "type": "string"
+ },
+ "markadsefniSlod": {
+ "type": "string"
+ },
+ "fannst": {
+ "type": "boolean"
+ }
+ }
+ },
+ "HeimagistingarModel": {
+ "type": "object",
+ "properties": {
+ "leyfi": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/HeimagistingDetail"
+ }
+ }
+ }
+ },
+ "HeimagistingDetail": {
+ "type": "object",
+ "properties": {
+ "tegund": {
+ "type": "string"
+ },
+ "numerLeyfist": {
+ "type": "string"
+ },
+ "virkt": {
+ "type": "boolean"
+ },
+ "utgefid": {
+ "type": "string"
+ }
+ }
+ },
"VirkarHeimagistingar": {
"type": "object",
"properties": {
@@ -4012,6 +4219,12 @@
},
"skyring": {
"type": "string"
+ },
+ "simi": {
+ "type": "string"
+ },
+ "netfang": {
+ "type": "string"
}
}
},
diff --git a/libs/clients/syslumenn/src/lib/syslumennClient.service.ts b/libs/clients/syslumenn/src/lib/syslumennClient.service.ts
index 92dcdcb6990c..d606178c5ff7 100644
--- a/libs/clients/syslumenn/src/lib/syslumennClient.service.ts
+++ b/libs/clients/syslumenn/src/lib/syslumennClient.service.ts
@@ -1,80 +1,83 @@
+import { Inject, Injectable } from '@nestjs/common'
+import startOfDay from 'date-fns/startOfDay'
+
+import { AuthHeaderMiddleware } from '@island.is/auth-nest-tools'
+import { createEnhancedFetch } from '@island.is/clients/middlewares'
+
+import {
+ Configuration,
+ InnsigludSkjol,
+ LogradamadurSvar,
+ Skilabod,
+ SvarSkeyti,
+ SyslumennApi,
+ VedbandayfirlitRegluverkGeneralSvar,
+ VedbondTegundAndlags,
+ VirkLeyfiGetRequest,
+} from '../../gen/fetch'
+import { SyslumennClientConfig } from './syslumennClient.config'
import {
AlcoholLicence,
- SyslumennAuction,
- Homestay,
- PaginatedOperatingLicenses,
- OperatingLicensesCSV,
+ AssetName,
+ AssetType,
+ Attachment,
+ Broker,
CertificateInfoResponse,
- DistrictCommissionerAgencies,
DataUploadResponse,
- Person,
- Attachment,
- AssetType,
- MortgageCertificate,
- MortgageCertificateValidation,
- AssetName,
+ DistrictCommissionerAgencies,
+ EstateInfo,
EstateRegistrant,
EstateRelations,
- EstateInfo,
- RealEstateAgent,
+ Homestay,
+ InheritanceReportInfo,
+ InheritanceTax,
Lawyer,
- Broker,
+ ManyPropertyDetail,
+ MortgageCertificate,
+ MortgageCertificateValidation,
+ OperatingLicensesCSV,
+ PaginatedOperatingLicenses,
+ Person,
PropertyDetail,
+ RealEstateAgent,
+ RegistryPerson,
+ SyslumennAuction,
TemporaryEventLicence,
VehicleRegistration,
- RegistryPerson,
- InheritanceTax,
- InheritanceReportInfo,
- ManyPropertyDetail,
} from './syslumennClient.types'
import {
- mapSyslumennAuction,
- mapHomestay,
- mapPaginatedOperatingLicenses,
- mapOperatingLicensesCSV,
- mapCertificateInfo,
- mapDistrictCommissionersAgenciesResponse,
- mapDataUploadResponse,
+ cleanPropertyNumber,
constructUploadDataObject,
+ mapAlcoholLicence,
mapAssetName,
- mapEstateRegistrant,
- mapEstateInfo,
- mapRealEstateAgent,
- mapLawyer,
mapBroker,
- mapAlcoholLicence,
- cleanPropertyNumber,
- mapTemporaryEventLicence,
- mapMasterLicence,
- mapVehicle,
+ mapCertificateInfo,
+ mapDataUploadResponse,
mapDepartedToRegistryPerson,
- mapInheritanceTax,
+ mapDistrictCommissionersAgenciesResponse,
+ mapEstateInfo,
+ mapEstateRegistrant,
mapEstateToInheritanceReportInfo,
+ mapHomestay,
+ mapInheritanceTax,
mapJourneymanLicence,
+ mapLawyer,
+ mapMasterLicence,
+ mapOperatingLicensesCSV,
+ mapPaginatedOperatingLicenses,
mapProfessionRight,
- mapVehicleResponse,
+ mapPropertyCertificate,
+ mapRealEstateAgent,
mapRealEstateResponse,
mapShipResponse,
- mapPropertyCertificate,
+ mapSyslumennAuction,
+ mapTemporaryEventLicence,
+ mapVehicle,
+ mapVehicleResponse,
} from './syslumennClient.utils'
-import { Injectable, Inject } from '@nestjs/common'
-import {
- SyslumennApi,
- SvarSkeyti,
- Configuration,
- VirkLeyfiGetRequest,
- VedbondTegundAndlags,
- Skilabod,
- VedbandayfirlitRegluverkGeneralSvar,
- InnsigludSkjol,
-} from '../../gen/fetch'
-import { SyslumennClientConfig } from './syslumennClient.config'
-import type { ConfigType } from '@island.is/nest/config'
-import { AuthHeaderMiddleware } from '@island.is/auth-nest-tools'
-import { createEnhancedFetch } from '@island.is/clients/middlewares'
+import type { ConfigType } from '@island.is/nest/config'
const UPLOAD_DATA_SUCCESS = 'Gögn móttekin'
-
@Injectable()
export class SyslumennService {
constructor(
@@ -674,4 +677,22 @@ export class SyslumennService {
kennitala: nationalId,
})
}
+
+ async checkIfDelegationExists(
+ toNationalId: string,
+ fromNationalId: string,
+ ): Promise {
+ const { id, api } = await this.createApi()
+ const delegations: LogradamadurSvar[] = await api.logradamadurGet({
+ audkenni: id,
+ kennitala: toNationalId,
+ })
+
+ return delegations.some(
+ (delegation) =>
+ delegation.kennitala === fromNationalId &&
+ (!delegation.gildirTil ||
+ delegation.gildirTil > startOfDay(new Date())),
+ )
+ }
}
diff --git a/libs/clients/zendesk/src/lib/zendesk.service.ts b/libs/clients/zendesk/src/lib/zendesk.service.ts
index 13739a9c89ce..3927ee2ea8cf 100644
--- a/libs/clients/zendesk/src/lib/zendesk.service.ts
+++ b/libs/clients/zendesk/src/lib/zendesk.service.ts
@@ -5,7 +5,16 @@ import { LOGGER_PROVIDER } from '@island.is/logging'
export const ZENDESK_OPTIONS = 'ZENDESK_OPTIONS'
-export type Ticket = {
+export enum TicketStatus {
+ Open = 'open',
+ Pending = 'pending',
+ Solved = 'solved',
+ Closed = 'closed',
+ New = 'new',
+ OnHold = 'on-hold',
+}
+
+export type SubmitTicketInput = {
subject?: string
message: string
requesterId?: number
@@ -18,6 +27,13 @@ export type User = {
id: number
}
+export type Ticket = {
+ id: string
+ status: TicketStatus | string
+ custom_fields: Array<{ id: number; value: string }>
+ tags: Array
+}
+
export interface ZendeskServiceOptions {
email: string
token: string
@@ -121,7 +137,7 @@ export class ZendeskService {
subject,
requesterId,
tags = [],
- }: Ticket): Promise {
+ }: SubmitTicketInput): Promise {
const newTicket = JSON.stringify({
ticket: {
requester_id: requesterId,
@@ -146,4 +162,23 @@ export class ZendeskService {
return true
}
+
+ async getTicket(ticketId: string): Promise {
+ try {
+ const response = await axios.get(`${this.api}/tickets/${ticketId}.json`, {
+ ...this.params,
+ })
+
+ return response.data.ticket
+ } catch (e) {
+ const errMsg = 'Failed to get Zendesk ticket'
+ const description = e.response.data.description
+
+ this.logger.error(errMsg, {
+ message: description,
+ })
+
+ throw new Error(`${errMsg}: ${description}`)
+ }
+ }
}
diff --git a/libs/cms/src/lib/cms.contentful.service.ts b/libs/cms/src/lib/cms.contentful.service.ts
index 7b48437abb7a..55f256689fce 100644
--- a/libs/cms/src/lib/cms.contentful.service.ts
+++ b/libs/cms/src/lib/cms.contentful.service.ts
@@ -310,11 +310,19 @@ export class CmsContentfulService {
)
}
- async getOrganization(slug: string, lang: string): Promise {
+ async getOrganization(
+ slug: string,
+ lang: string,
+ ): Promise {
+ if (!slug) {
+ return null
+ }
+
const params = {
['content_type']: 'organization',
include: 10,
'fields.slug': slug,
+ limit: 1,
}
const result = await this.contentfulRepository
diff --git a/libs/island-ui/core/src/lib/IconRC/iconMap.ts b/libs/island-ui/core/src/lib/IconRC/iconMap.ts
index 5d92e500e1f1..e3e549ffa12b 100644
--- a/libs/island-ui/core/src/lib/IconRC/iconMap.ts
+++ b/libs/island-ui/core/src/lib/IconRC/iconMap.ts
@@ -90,6 +90,7 @@ export type Icon =
| 'swapVertical'
| 'thumbsUp'
| 'thumbsDown'
+ | 'leaf'
export default {
filled: {
@@ -183,6 +184,7 @@ export default {
swapVertical: 'SwapVertical',
thumbsUp: 'ThumbsUp',
thumbsDown: 'ThumbsDown',
+ leaf: 'Leaf',
},
outline: {
archive: 'ArchiveOutline',
@@ -275,5 +277,6 @@ export default {
swapVertical: 'SwapVertical',
thumbsUp: 'ThumbsUpOutline',
thumbsDown: 'ThumbsDownOutline',
+ leaf: 'LeafOutline',
},
}
diff --git a/libs/island-ui/core/src/lib/IconRC/icons/Leaf.tsx b/libs/island-ui/core/src/lib/IconRC/icons/Leaf.tsx
new file mode 100644
index 000000000000..317a37b0ff6c
--- /dev/null
+++ b/libs/island-ui/core/src/lib/IconRC/icons/Leaf.tsx
@@ -0,0 +1,22 @@
+import * as React from 'react'
+import type { SvgProps as SVGRProps } from '../types'
+
+const Leaf = ({
+ title,
+ titleId,
+ ...props
+}: React.SVGProps & SVGRProps) => {
+ return (
+
+ )
+}
+
+export default Leaf
diff --git a/libs/island-ui/core/src/lib/IconRC/icons/LeafOutline.tsx b/libs/island-ui/core/src/lib/IconRC/icons/LeafOutline.tsx
new file mode 100644
index 000000000000..717606a049d7
--- /dev/null
+++ b/libs/island-ui/core/src/lib/IconRC/icons/LeafOutline.tsx
@@ -0,0 +1,29 @@
+import * as React from 'react'
+import type { SvgProps as SVGRProps } from '../types'
+
+const LeafOutline = ({
+ title,
+ titleId,
+ ...props
+}: React.SVGProps & SVGRProps) => {
+ return (
+
+ )
+}
+
+export default LeafOutline
diff --git a/libs/judicial-system/types/src/lib/institution.ts b/libs/judicial-system/types/src/lib/institution.ts
index ad1bbaf14839..cf5ed46827d7 100644
--- a/libs/judicial-system/types/src/lib/institution.ts
+++ b/libs/judicial-system/types/src/lib/institution.ts
@@ -16,4 +16,5 @@ export interface Institution {
defaultCourtId?: string
policeCaseNumberPrefix?: string
nationalId?: string
+ address?: string
}
diff --git a/libs/portals/admin/delegation-admin/src/components/DelegationList.tsx b/libs/portals/admin/delegation-admin/src/components/DelegationList.tsx
index b248b3e20505..b8e47dc9168d 100644
--- a/libs/portals/admin/delegation-admin/src/components/DelegationList.tsx
+++ b/libs/portals/admin/delegation-admin/src/components/DelegationList.tsx
@@ -1,6 +1,8 @@
import { AuthCustomDelegation } from '@island.is/api/schema'
import { Box, Stack } from '@island.is/island-ui/core'
import { AccessCard } from '@island.is/portals/shared-modules/delegations'
+import { useDeleteCustomDelegationAdminMutation } from '../screens/DelegationAdminDetails/DelegationAdmin.generated'
+import { useRevalidator } from 'react-router-dom'
interface DelegationProps {
direction: 'incoming' | 'outgoing'
@@ -8,20 +10,34 @@ interface DelegationProps {
}
const DelegationList = ({ delegationsList, direction }: DelegationProps) => {
+ const [deleteCustomDelegationAdminMutation] =
+ useDeleteCustomDelegationAdminMutation()
+ const { revalidate } = useRevalidator()
+
return (
- {delegationsList.map((delegation) => (
- {
- console.warn('Delete delegation')
- }}
- />
- ))}
+ {delegationsList.map((delegation) => {
+ return (
+ {
+ const { data } = await deleteCustomDelegationAdminMutation({
+ variables: {
+ id: delegation.id as string,
+ },
+ })
+ if (data) {
+ revalidate()
+ }
+ }}
+ />
+ )
+ })}
)
diff --git a/libs/portals/admin/delegation-admin/src/lib/messages.ts b/libs/portals/admin/delegation-admin/src/lib/messages.ts
index 4ce4b5e4638c..f6377eff0302 100644
--- a/libs/portals/admin/delegation-admin/src/lib/messages.ts
+++ b/libs/portals/admin/delegation-admin/src/lib/messages.ts
@@ -19,7 +19,7 @@ export const m = defineMessages({
},
createNewDelegation: {
id: 'admin.delegationAdmin:delegationAdminCreateNewDelegation',
- defaultMessage: 'Stofna nýtt umboð',
+ defaultMessage: 'Skrá nýtt umboð',
},
delegationFrom: {
id: 'admin.delegationAdmin:delegationAdminDelegationFrom',
@@ -79,7 +79,7 @@ export const m = defineMessages({
},
referenceId: {
id: 'admin.delegationAdmin:referenceId',
- defaultMessage: 'Númer mála í Zendesk',
+ defaultMessage: 'Númer máls í Zendesk',
},
errorDefault: {
id: 'admin.delegationAdmin:errorDefault',
@@ -117,4 +117,8 @@ export const m = defineMessages({
id: 'admin.delegationAdmin:createDelegationConfirmModalTitle',
defaultMessage: 'Þú ert að skrá nýtt umboð',
},
+ createDelegationSuccessToast: {
+ id: 'admin.delegationAdmin:createDelegationSuccessToast',
+ defaultMessage: 'Umboð var skráð',
+ },
})
diff --git a/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.action.ts b/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.action.ts
index 9d85e6cf80ea..83aac0a774f7 100644
--- a/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.action.ts
+++ b/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.action.ts
@@ -1,7 +1,6 @@
import { z } from 'zod'
import kennitala from 'kennitala'
import isFuture from 'date-fns/isFuture'
-import { redirect } from 'react-router-dom'
import { WrappedActionFn } from '@island.is/portals/core'
import {
validateFormData,
@@ -12,7 +11,6 @@ import {
CreateDelegationMutation,
CreateDelegationMutationVariables,
} from './CreateDelegation.generated'
-import { DelegationAdminPaths } from '../../lib/paths'
const schema = z
.object({
@@ -52,6 +50,7 @@ export type CreateDelegationResult = ValidateFormDataResult & {
* Global error message if the mutation fails
*/
globalError?: boolean
+ success?: boolean
}
export const createDelegationAction: WrappedActionFn =
@@ -83,8 +82,14 @@ export const createDelegationAction: WrappedActionFn =
},
})
- return redirect(DelegationAdminPaths.Root)
+ return {
+ errors: null,
+ data: null,
+ globalError: false,
+ success: true,
+ }
} catch (e) {
+ console.error(e)
return {
errors: null,
data: null,
diff --git a/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.tsx b/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.tsx
index 74adeb5e4355..c7ec091a643f 100644
--- a/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.tsx
+++ b/libs/portals/admin/delegation-admin/src/screens/CreateDelegation/CreateDelegation.tsx
@@ -19,9 +19,10 @@ import { IntroHeader, m as coreMessages } from '@island.is/portals/core'
import { m } from '../../lib/messages'
import { DelegationAdminPaths } from '../../lib/paths'
import NumberFormat from 'react-number-format'
-
+import startOfDay from 'date-fns/startOfDay'
import {
Form,
+ redirect,
useActionData,
useNavigate,
useSearchParams,
@@ -39,8 +40,9 @@ import {
import { CreateDelegationConfirmModal } from '../../components/CreateDelegationConfirmModal'
import { Identity } from '@island.is/api/schema'
import kennitala from 'kennitala'
-import { unmaskString } from '@island.is/shared/utils'
+import { maskString, unmaskString } from '@island.is/shared/utils'
import { useAuth } from '@island.is/auth/react'
+import { replaceParams } from '@island.is/react-spa/shared'
const CreateDelegationScreen = () => {
const { formatMessage } = useLocale()
@@ -72,7 +74,32 @@ const CreateDelegationScreen = () => {
},
]
+ async function success() {
+ try {
+ const maskedNationalId = await maskString(
+ fromIdentity?.nationalId ?? '',
+ userInfo?.profile.nationalId ?? '',
+ )
+ successToast()
+ navigate(
+ replaceParams({
+ href: DelegationAdminPaths.DelegationAdmin,
+ params: {
+ nationalId: maskedNationalId ?? '',
+ },
+ }),
+ { replace: true },
+ )
+ } catch (e) {
+ navigate(DelegationAdminPaths.Root)
+ }
+ }
+
useEffect(() => {
+ if (actionData?.success) {
+ success()
+ }
+
if (actionData?.data && !actionData.errors) {
setIsConfirmed(true)
setShowConfirmModal(true)
@@ -95,13 +122,17 @@ const CreateDelegationScreen = () => {
}
}
- getFromNationalId()
+ !!defaultFromNationalId && getFromNationalId()
}, [defaultFromNationalId])
const noUserFoundToast = () => {
toast.warning(formatMessage(m.grantIdentityError))
}
+ const successToast = () => {
+ toast.success(formatMessage(m.createDelegationSuccessToast))
+ }
+
const [getFromIdentity, { loading: fromIdentityQueryLoading }] =
useIdentityLazyQuery({
onError: (error) => {
@@ -339,6 +370,8 @@ const CreateDelegationScreen = () => {
errorMessage={formatMessage(
m[actionData?.errors?.validTo as keyof typeof m],
)}
+ minDate={startOfDay(new Date())}
+ appearInline
/>
{
return (
navigate(DelegationAdminPaths.Root)} />
-
-
-
-
+
+
{hasAdminAccess && (
-
-
+
+
)}
-
+
+
{
{
label: formatMessage(m.delegationFrom),
content:
- delegationAdmin.incoming.length > 0 ? (
+ delegationAdmin.outgoing.length > 0 ? (
) : (
@@ -90,11 +89,11 @@ const DelegationAdminScreen = () => {
{
label: formatMessage(m.delegationTo),
content:
- delegationAdmin.outgoing.length > 0 ? (
+ delegationAdmin.incoming.length > 0 ? (
) : (
diff --git a/libs/portals/admin/delegation-admin/src/screens/Root.tsx b/libs/portals/admin/delegation-admin/src/screens/Root.tsx
index 00e929ee9106..3dd3600dd6c8 100644
--- a/libs/portals/admin/delegation-admin/src/screens/Root.tsx
+++ b/libs/portals/admin/delegation-admin/src/screens/Root.tsx
@@ -7,6 +7,7 @@ import {
Button,
GridColumn,
GridRow,
+ Box,
} from '@island.is/island-ui/core'
import React, { useEffect, useState } from 'react'
import { useSubmitting } from '@island.is/react-spa/shared'
@@ -48,25 +49,31 @@ const Root = () => {
return (
<>
-
-
-
-
+
{hasAdminAccess && (
-
-
+
+
)}
-
+
+
+
+
` as HTMLText
const gildistaka =
`Reglugerð þessi er sett með heimild í [].
Reglugerðin öðlast þegar gildi.
` as HTMLText
return [text, gildistaka]
@@ -232,18 +230,8 @@ export const formatAmendingRegBody = (
if (element.classList.contains('article__title')) {
const clone = element.cloneNode(true)
- if (clone instanceof Element) {
- const emElement = clone.querySelector('em')
- if (emElement) {
- emElement.parentNode?.removeChild(emElement)
- }
-
- const textContent = clone.textContent?.trim() ?? ''
-
- articleTitle = textContent
- } else {
- articleTitle = element.innerText
- }
+ const textContent = getTextWithSpaces(clone)
+ articleTitle = extractArticleTitleDisplay(textContent)
testGroup.title = articleTitle
isArticleTitle = true
paragraph = 0 // Reset paragraph count for the new article
@@ -385,10 +373,8 @@ export const formatAmendingRegBody = (
if (testGroup.isDeletion === true) {
const articleTitleNumber = testGroup.title
- const grMatch = articleTitleNumber.match(/^\d+\. gr\./)
- const articleTitleDisplay = grMatch ? grMatch[0] : articleTitleNumber
additionArray.push([
- `${articleTitleDisplay} ${regNameDisplay} fellur brott.
` as HTMLText,
+ `${articleTitleNumber} ${regNameDisplay} fellur brott.
` as HTMLText,
])
} else if (testGroup.isAddition === true) {
let prevArticleTitle = ''
@@ -401,7 +387,8 @@ export const formatAmendingRegBody = (
? flatten(testGroup.original)
: []
- const prevArticleTitleNumber = prevArticleTitle.match(/^\d+\. gr\./)
+ const prevArticleTitleNumber =
+ extractArticleTitleDisplay(prevArticleTitle)
let articleDisplayText = ''
@@ -449,7 +436,11 @@ export const formatAmendingBodyWithArticlePrefix = (
const additions = flatten(impactAdditionArray)
- const htmlForEditor = formatAffectedAndPlaceAffectedAtEnd(additions)
+ const hideAffected = allSameDay(additions)
+ const htmlForEditor = formatAffectedAndPlaceAffectedAtEnd(
+ additions,
+ hideAffected,
+ )
const returnArray = compact(htmlForEditor)
diff --git a/libs/portals/admin/regulations-admin/src/utils/formatAmendingUtils.ts b/libs/portals/admin/regulations-admin/src/utils/formatAmendingUtils.ts
new file mode 100644
index 000000000000..007246a3332a
--- /dev/null
+++ b/libs/portals/admin/regulations-admin/src/utils/formatAmendingUtils.ts
@@ -0,0 +1,89 @@
+import isSameDay from 'date-fns/isSameDay'
+import { HTMLText } from '@island.is/regulations'
+
+export const groupElementsByArticleTitleFromDiv = (
+ div: HTMLDivElement,
+): HTMLElement[][] => {
+ const result: HTMLElement[][] = []
+ let currentGroup: HTMLElement[] = []
+
+ Array.from(div.children).forEach((child) => {
+ const element = child as HTMLElement
+ if (
+ element.classList.contains('article__title') ||
+ element.classList.contains('chapter__title')
+ ) {
+ if (currentGroup.length > 0) {
+ result.push(currentGroup)
+ }
+ currentGroup = [element]
+ } else {
+ currentGroup.push(element)
+ }
+ })
+
+ if (currentGroup.length > 0) {
+ result.push(currentGroup)
+ }
+
+ return result
+}
+
+/**
+ * Extracts article title number (e.g., '1. gr.' or '1. gr. a') from a string, allowing for Icelandic characters.
+ */
+export const extractArticleTitleDisplay = (title: string): string => {
+ const grMatch = title.match(/^\d+\. gr\.(?: [\p{L}])?(?= |$)/u)
+ const articleTitleDisplay = grMatch ? grMatch[0] : title
+ return articleTitleDisplay
+}
+
+export const getTextWithSpaces = (element: Node): string => {
+ let result = ''
+
+ element.childNodes.forEach((node, index) => {
+ if (node.nodeType === Node.TEXT_NODE) {
+ result += (node.textContent?.trim() || '') + ' '
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
+ result += getTextWithSpaces(node as HTMLElement)
+
+ // If the current element is not the last node and the next node is also an element or text node,
+ // add a space between elements
+ if (
+ element.childNodes[index + 1] &&
+ element.childNodes[index + 1].nodeType !== Node.COMMENT_NODE
+ ) {
+ result += ' '
+ }
+ }
+ })
+
+ return result.trim() // Trim any excess space
+}
+
+export const removeRegPrefix = (title: string) => {
+ if (/^Reglugerð/.test(title)) {
+ return title.replace(/^Reglugerð/, '')
+ }
+ return title
+}
+
+export const isGildisTaka = (str: string) => {
+ return /(öðlast|tekur).*gildi|sett.*með.*(?:heimild|stoð)/.test(
+ (str || '').toLowerCase(),
+ )
+}
+
+export type AdditionObject = {
+ formattedRegBody: HTMLText[]
+ date: Date | undefined
+}
+
+export const allSameDay = (objects: AdditionObject[]): boolean => {
+ const validObjects = objects.filter((obj) => obj.date !== undefined)
+
+ if (validObjects.length === 0) return true
+ const firstDate = validObjects[0].date!
+
+ return validObjects.every((obj) => isSameDay(obj.date!, firstDate))
+}
diff --git a/libs/portals/admin/regulations-admin/src/utils/groupByArticleTitle.ts b/libs/portals/admin/regulations-admin/src/utils/groupByArticleTitle.ts
deleted file mode 100644
index 96e064372852..000000000000
--- a/libs/portals/admin/regulations-admin/src/utils/groupByArticleTitle.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-export const groupElementsByArticleTitleFromDiv = (
- div: HTMLDivElement,
-): HTMLElement[][] => {
- const result: HTMLElement[][] = []
- let currentGroup: HTMLElement[] = []
-
- Array.from(div.children).forEach((child) => {
- const element = child as HTMLElement
- if (element.classList.contains('article__title')) {
- if (currentGroup.length > 0) {
- result.push(currentGroup)
- }
- currentGroup = [element]
- } else {
- currentGroup.push(element)
- }
- })
-
- if (currentGroup.length > 0) {
- result.push(currentGroup)
- }
-
- return result
-}
diff --git a/libs/portals/admin/signature-collection/src/lib/messages.ts b/libs/portals/admin/signature-collection/src/lib/messages.ts
index 5f2efaaa5285..f8381a48deb3 100644
--- a/libs/portals/admin/signature-collection/src/lib/messages.ts
+++ b/libs/portals/admin/signature-collection/src/lib/messages.ts
@@ -12,6 +12,16 @@ export const m = defineMessages({
defaultMessage: 'Meðmælasafnanir',
description: '',
},
+ signatureListsTitlePresidential: {
+ id: 'admin-portal.signature-collection-parliamentary:signatureLists',
+ defaultMessage: 'Forsetakosningar',
+ description: '',
+ },
+ signatureListsConstituencyTitle: {
+ id: 'admin-portal.signature-collection:signatureListsConstituencyTitle',
+ defaultMessage: 'Kjördæmi',
+ description: '',
+ },
signatureListsDescription: {
id: 'admin-portal.signature-collection:signatureListsDescription',
defaultMessage:
@@ -150,6 +160,11 @@ export const m = defineMessages({
defaultMessage: 'Skoða söfnun',
description: '',
},
+ viewConstituency: {
+ id: 'admin-portal.signature-collection:viewConstituency',
+ defaultMessage: 'Skoða kjördæmi',
+ description: '',
+ },
noLists: {
id: 'admin-portal.signature-collection:noLists',
defaultMessage: 'Engin söfnun í gangi',
@@ -177,6 +192,38 @@ export const m = defineMessages({
description: '',
},
+ /* Hætta við söfnun modal */
+ cancelCollectionButton: {
+ id: 'dmin-portal.signature-collection:cancelCollectionButton',
+ defaultMessage: 'Eyða lista',
+ description: '',
+ },
+ cancelCollectionModalMessage: {
+ id: 'dmin-portal.signature-collection:cancelCollectionModalMessage',
+ defaultMessage: 'Þú ert að fara að eyða þessum lista. Ertu viss?',
+ description: '',
+ },
+ cancelCollectionModalConfirmButton: {
+ id: 'dmin-portal.signature-collection:modalConfirmButton',
+ defaultMessage: 'Já, eyða lista',
+ description: '',
+ },
+ cancelCollectionModalCancelButton: {
+ id: 'dmin-portal.signature-collection:cancelCollectionModalCancelButton',
+ defaultMessage: 'Nei, hætta við',
+ description: '',
+ },
+ cancelCollectionModalToastError: {
+ id: 'dmin-portal.signature-collection:modalToastError',
+ defaultMessage: 'Ekki tókst að eyða lista',
+ description: '',
+ },
+ cancelCollectionModalToastSuccess: {
+ id: 'dmin-portal.signature-collection:cancelCollectionModalToastSuccess',
+ defaultMessage: 'Tókst að eyða lista',
+ description: '',
+ },
+
// View list
singleList: {
id: 'admin-portal.signature-collection:singleList',
@@ -220,6 +267,22 @@ export const m = defineMessages({
defaultMessage: 'Yfirlit meðmæla',
description: '',
},
+ downloadReports: {
+ id: 'admin-portal.signature-collection:downloadReports',
+ defaultMessage: 'Sækja skýrslur',
+ description: '',
+ },
+ downloadReportsDescription: {
+ id: 'admin-portal.signature-collection:downloadReportsDescription',
+ defaultMessage:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nec purus nec odio ultricies ultricies. Nullam nec purus nec odio ultricies ultricies.',
+ description: '',
+ },
+ downloadButton: {
+ id: 'admin-portal.signature-collection:downloadButton',
+ defaultMessage: 'Hlaða niður',
+ description: '',
+ },
searchInListPlaceholder: {
id: 'admin-portal.signature-collection:searchInListPlaceholder',
defaultMessage: 'Leitaðu að nafni eða kennitölu',
@@ -397,6 +460,11 @@ export const m = defineMessages({
defaultMessage: 'Samtals fjöldi',
description: '',
},
+ totalListsPerConstituency: {
+ id: 'admin-portal.signature-collection:totalListsPerConstituency',
+ defaultMessage: 'Fjöldi lista: ',
+ description: '',
+ },
nationalIdsSuccess: {
id: 'admin-portal.signature-collection:nationalIdsSuccess',
defaultMessage: 'Kennitölur sem tókst að hlaða upp',
@@ -417,7 +485,7 @@ export const m = defineMessages({
compareListsDescription: {
id: 'admin-portal.signature-collection:compareListsDescription',
defaultMessage:
- 'Fulltrúar í yfirkjörstjórnum og frambjóðendur geta ekki mælt með framboði.',
+ 'Fulltrúar í yfirkjörstjórnum og frambjóðendur geta ekki mælt með framboði',
description: '',
},
compareListsModalDescription: {
@@ -480,6 +548,116 @@ export const m = defineMessages({
defaultMessage: 'Loka lista',
description: '',
},
+ paperSigneesHeader: {
+ id: 'admin-portal.signature-collection:paperSigneesHeader',
+ defaultMessage: 'Skrá meðmæli af blaði',
+ description: '',
+ },
+ paperSigneesClearButton: {
+ id: 'admin-portal.signature-collection:paperSigneesClearButton',
+ defaultMessage: 'Hreinsa',
+ description: '',
+ },
+ paperNumber: {
+ id: 'admin-portal.signature-collection:paperNumber',
+ defaultMessage: 'Blaðsíðunúmer',
+ description: '',
+ },
+ editPaperNumber: {
+ id: 'admin-portal.signature-collection:editPaperNumber',
+ defaultMessage: 'Breyta blaðsíðunúmeri',
+ description: '',
+ },
+ editPaperNumberSuccess: {
+ id: 'admin-portal.signature-collection:editPaperNumberSuccess',
+ defaultMessage: 'Tókst að breyta blaðsíðunúmeri',
+ description: '',
+ },
+ editPaperNumberError: {
+ id: 'admin-portal.signature-collection:editPaperNumberSuccess',
+ defaultMessage: 'Ekki tókst að breyta blaðsíðunúmeri',
+ description: '',
+ },
+ saveEditPaperNumber: {
+ id: 'admin-portal.signature-collection:saveEditPaperNumber',
+ defaultMessage: 'Uppfæra blaðsíðunúmer',
+ description: '',
+ },
+ paperSigneeName: {
+ id: 'admin-portal.signature-collection:paperSigneeName',
+ defaultMessage: 'Nafn meðmælanda',
+ description: '',
+ },
+ signPaperSigneeButton: {
+ id: 'admin-portal.signature-collection:signPaperSigneeButton',
+ defaultMessage: 'Skrá meðmæli á lista',
+ description: '',
+ },
+ paperSigneeTypoTitle: {
+ id: 'admin-portal.signature-collection:paperSigneeTypoTitle',
+ defaultMessage: 'Kennitala ekki á réttu formi',
+ description: '',
+ },
+ paperSigneeTypoMessage: {
+ id: 'admin-portal.signature-collection:paperSigneeTypoMessage',
+ defaultMessage: 'Vinsamlegast athugið kennitöluna og reynið aftur',
+ description: '',
+ },
+ paperSigneeCantSignTitle: {
+ id: 'admin-portal.signature-collection:paperSigneeCantSignTitle',
+ defaultMessage: 'Ekki er hægt að skrá meðmæli',
+ description: '',
+ },
+ paperSigneeCantSignMessage: {
+ id: 'admin-portal.signature-collection:paperSigneeCantSign',
+ defaultMessage: 'Kennitala uppfyllir ekki skilyrði fyrir að skrá meðmæli',
+ description: '',
+ },
+ paperSigneeSuccess: {
+ id: 'admin-portal.signature-collection:paperSigneeSuccess',
+ defaultMessage: 'Meðmæli skráð',
+ description: '',
+ },
+ paperSigneeError: {
+ id: 'admin-portal.signature-collection:paperSigneeError',
+ defaultMessage: 'Ekki tókst að skrá meðmæli',
+ description: '',
+ },
+})
+
+export const parliamentaryMessages = defineMessages({
+ signatureListsTitle: {
+ id: 'admin-portal.signature-collection-parliamentary:signatureLists',
+ defaultMessage: 'Alþingiskosningar',
+ description: '',
+ },
+ signatureListsDescription: {
+ id: 'admin-portal.signature-collection-parliamentary:signatureListsDescription',
+ defaultMessage: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
+ description: '',
+ },
+ signatureListsIntro: {
+ id: 'admin-portal.signature-collection-parliamentary:signatureListsIntro',
+ defaultMessage:
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed etiam, ut inquit, in vita et in voluptate locum ponamus, isdem et in dolore et in odio.',
+ description: '',
+ },
+ compareListsButton: {
+ id: 'admin-portal.signature-collection-parliamentary:compareListsButton',
+ defaultMessage: 'Bera saman',
+ description: '',
+ },
+ compareListsDescription: {
+ id: 'admin-portal.signature-collection-parliamentary:compareListsDescription',
+ defaultMessage: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
+ description: '',
+ },
+ singleConstituencyIntro: {
+ id: 'admin-portal.signature-collection-parliamentary:singleConstituencyIntro',
+ defaultMessage:
+ 'Hér er yfirlit yfir allar meðmælasafnanir sem stofnaðar hafa verið í',
+ description: '',
+ },
})
export const createCollectionErrorMessages = defineMessages({
diff --git a/libs/portals/admin/signature-collection/src/lib/navigation.ts b/libs/portals/admin/signature-collection/src/lib/navigation.ts
index 3fd05cb1b1c7..c48d371f7453 100644
--- a/libs/portals/admin/signature-collection/src/lib/navigation.ts
+++ b/libs/portals/admin/signature-collection/src/lib/navigation.ts
@@ -1,18 +1,23 @@
import { PortalNavigationItem } from '@island.is/portals/core'
import { SignatureCollectionPaths } from './paths'
-import { m } from './messages'
+import { m, parliamentaryMessages } from './messages'
export const signatureCollectionNavigation: PortalNavigationItem = {
name: m.signatureListsTitle,
icon: {
- icon: 'settings',
+ icon: 'receipt',
},
description: m.signatureListsDescription,
- path: SignatureCollectionPaths.SignatureLists,
+ path: SignatureCollectionPaths.ParliamentaryRoot,
children: [
+ {
+ name: parliamentaryMessages.signatureListsTitle,
+ path: SignatureCollectionPaths.ParliamentaryRoot,
+ activeIfExact: true,
+ },
{
name: m.collectionTitle,
- path: SignatureCollectionPaths.SignatureLists,
+ path: SignatureCollectionPaths.PresidentialLists,
activeIfExact: true,
},
],
diff --git a/libs/portals/admin/signature-collection/src/lib/paths.ts b/libs/portals/admin/signature-collection/src/lib/paths.ts
index de55e159b3e2..4627cffeaa4c 100644
--- a/libs/portals/admin/signature-collection/src/lib/paths.ts
+++ b/libs/portals/admin/signature-collection/src/lib/paths.ts
@@ -1,4 +1,10 @@
export enum SignatureCollectionPaths {
- SignatureLists = '/medmaelasofnun',
- SignatureList = '/medmaelasofnun/:id',
+ // Presidential
+ PresidentialLists = '/medmaelasofnun',
+ PresidentialList = '/medmaelasofnun/:listId',
+
+ // Parliamentary
+ ParliamentaryRoot = '/althingiskosningar',
+ ParliamentaryConstituency = '/althingiskosningar/:constituencyName',
+ ParliamentaryConstituencyList = '/althingiskosningar/:constituencyName/:listId',
}
diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/AllLists.loader.ts b/libs/portals/admin/signature-collection/src/loaders/AllLists.loader.ts
similarity index 89%
rename from libs/portals/admin/signature-collection/src/screens/AllLists/AllLists.loader.ts
rename to libs/portals/admin/signature-collection/src/loaders/AllLists.loader.ts
index ce93146aa674..9b123c9a44f2 100644
--- a/libs/portals/admin/signature-collection/src/screens/AllLists/AllLists.loader.ts
+++ b/libs/portals/admin/signature-collection/src/loaders/AllLists.loader.ts
@@ -1,8 +1,4 @@
import type { WrappedLoaderFn } from '@island.is/portals/core'
-import {
- AllListsDocument,
- AllListsQuery,
-} from './graphql/getAllSignatureLists.generated'
import {
SignatureCollection,
SignatureCollectionList,
@@ -10,7 +6,11 @@ import {
import {
CollectionDocument,
CollectionQuery,
-} from './graphql/getCollectionStatus.generated'
+} from './allListsGraphql/getCollectionStatus.generated'
+import {
+ AllListsDocument,
+ AllListsQuery,
+} from './allListsGraphql/getAllSignatureLists.generated'
export interface ListsLoaderReturn {
allLists: SignatureCollectionList[]
@@ -19,9 +19,7 @@ export interface ListsLoaderReturn {
}
export const listsLoader: WrappedLoaderFn = ({ client }) => {
- return async ({
- params,
- }): Promise<{
+ return async (): Promise<{
allLists: SignatureCollectionList[]
collectionStatus: string
collection: SignatureCollection
diff --git a/libs/portals/admin/signature-collection/src/screens/List/List.loader.ts b/libs/portals/admin/signature-collection/src/loaders/List.loader.ts
similarity index 85%
rename from libs/portals/admin/signature-collection/src/screens/List/List.loader.ts
rename to libs/portals/admin/signature-collection/src/loaders/List.loader.ts
index 174eb769cc41..67ca3e477350 100644
--- a/libs/portals/admin/signature-collection/src/screens/List/List.loader.ts
+++ b/libs/portals/admin/signature-collection/src/loaders/List.loader.ts
@@ -1,20 +1,20 @@
import type { WrappedLoaderFn } from '@island.is/portals/core'
-import {
- ListbyidDocument,
- ListbyidQuery,
-} from './graphql/getSignatureList.generated'
import {
SignatureCollectionList,
SignatureCollectionSignature,
} from '@island.is/api/schema'
import {
- ListStatusDocument,
- ListStatusQuery,
-} from './graphql/getListStatus.generated'
+ ListbyidDocument,
+ ListbyidQuery,
+} from './listGraphql/getSignatureList.generated'
import {
SignaturesDocument,
SignaturesQuery,
-} from './graphql/getListSignees.generated'
+} from './listGraphql/getListSignees.generated'
+import {
+ ListStatusDocument,
+ ListStatusQuery,
+} from './listGraphql/getListStatus.generated'
export const listLoader: WrappedLoaderFn = ({ client }) => {
return async ({
@@ -29,7 +29,7 @@ export const listLoader: WrappedLoaderFn = ({ client }) => {
fetchPolicy: 'network-only',
variables: {
input: {
- listId: params.id,
+ listId: params.listId,
},
},
})
@@ -39,7 +39,7 @@ export const listLoader: WrappedLoaderFn = ({ client }) => {
fetchPolicy: 'network-only',
variables: {
input: {
- listId: params.id,
+ listId: params.listId,
},
},
})
@@ -49,7 +49,7 @@ export const listLoader: WrappedLoaderFn = ({ client }) => {
fetchPolicy: 'network-only',
variables: {
input: {
- listId: params.id,
+ listId: params.listId,
},
},
})
diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/graphql/getAllSignatureLists.graphql b/libs/portals/admin/signature-collection/src/loaders/allListsGraphql/getAllSignatureLists.graphql
similarity index 100%
rename from libs/portals/admin/signature-collection/src/screens/AllLists/graphql/getAllSignatureLists.graphql
rename to libs/portals/admin/signature-collection/src/loaders/allListsGraphql/getAllSignatureLists.graphql
diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/graphql/getCollectionStatus.graphql b/libs/portals/admin/signature-collection/src/loaders/allListsGraphql/getCollectionStatus.graphql
similarity index 100%
rename from libs/portals/admin/signature-collection/src/screens/AllLists/graphql/getCollectionStatus.graphql
rename to libs/portals/admin/signature-collection/src/loaders/allListsGraphql/getCollectionStatus.graphql
diff --git a/libs/portals/admin/signature-collection/src/screens/List/graphql/getListSignees.graphql b/libs/portals/admin/signature-collection/src/loaders/listGraphql/getListSignees.graphql
similarity index 100%
rename from libs/portals/admin/signature-collection/src/screens/List/graphql/getListSignees.graphql
rename to libs/portals/admin/signature-collection/src/loaders/listGraphql/getListSignees.graphql
diff --git a/libs/portals/admin/signature-collection/src/screens/List/graphql/getListStatus.graphql b/libs/portals/admin/signature-collection/src/loaders/listGraphql/getListStatus.graphql
similarity index 100%
rename from libs/portals/admin/signature-collection/src/screens/List/graphql/getListStatus.graphql
rename to libs/portals/admin/signature-collection/src/loaders/listGraphql/getListStatus.graphql
diff --git a/libs/portals/admin/signature-collection/src/screens/List/graphql/getSignatureList.graphql b/libs/portals/admin/signature-collection/src/loaders/listGraphql/getSignatureList.graphql
similarity index 100%
rename from libs/portals/admin/signature-collection/src/screens/List/graphql/getSignatureList.graphql
rename to libs/portals/admin/signature-collection/src/loaders/listGraphql/getSignatureList.graphql
diff --git a/libs/portals/admin/signature-collection/src/module.tsx b/libs/portals/admin/signature-collection/src/module.tsx
index 7bd7a7a410b1..3bdd8fd55999 100644
--- a/libs/portals/admin/signature-collection/src/module.tsx
+++ b/libs/portals/admin/signature-collection/src/module.tsx
@@ -2,12 +2,20 @@ import { PortalModule } from '@island.is/portals/core'
import { lazy } from 'react'
import { m } from './lib/messages'
import { SignatureCollectionPaths } from './lib/paths'
-import { listsLoader } from './screens/AllLists/AllLists.loader'
import { AdminPortalScope } from '@island.is/auth/scopes'
-import { listLoader } from './screens/List/List.loader'
+import { listsLoader } from './loaders/AllLists.loader'
+import { listLoader } from './loaders/List.loader'
-const AllLists = lazy(() => import('./screens/AllLists'))
-const List = lazy(() => import('./screens/List'))
+/* parliamentary */
+const ParliamentaryRoot = lazy(() => import('./screens-parliamentary'))
+const ParliamentaryConstituency = lazy(() =>
+ import('./screens-parliamentary/Constituency'),
+)
+const ParliamentaryList = lazy(() => import('./screens-parliamentary/List'))
+
+/* presidential */
+const AllLists = lazy(() => import('./screens-presidential/AllLists'))
+const List = lazy(() => import('./screens-presidential/List'))
const allowedScopes: string[] = [
AdminPortalScope.signatureCollectionManage,
@@ -20,9 +28,30 @@ export const signatureCollectionModule: PortalModule = {
enabled: ({ userInfo }) =>
userInfo.scopes.some((scope) => allowedScopes.includes(scope)),
routes: (props) => [
+ /* ------ Parliamentary ------ */
+ {
+ name: m.signatureListsTitle,
+ path: SignatureCollectionPaths.ParliamentaryRoot,
+ element: ,
+ loader: listsLoader(props),
+ },
+ {
+ name: m.signatureListsConstituencyTitle,
+ path: SignatureCollectionPaths.ParliamentaryConstituency,
+ element: ,
+ loader: listsLoader(props),
+ },
+ {
+ name: m.singleList,
+ path: SignatureCollectionPaths.ParliamentaryConstituencyList,
+ element: ,
+ loader: listLoader(props),
+ },
+
+ /* ------ Presidential ------ */
{
name: m.signatureListsTitle,
- path: SignatureCollectionPaths.SignatureLists,
+ path: SignatureCollectionPaths.PresidentialLists,
element: (
{
+ const { formatMessage } = useLocale()
+ const navigate = useNavigate()
+
+ const { collection, allLists } = useLoaderData() as ListsLoaderReturn
+ const { constituencyName } = useParams() as { constituencyName: string }
+
+ const constituencyLists = allLists.filter(
+ (list) => list.area.name === constituencyName,
+ )
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatMessage(m.totalListResults) +
+ ': ' +
+ constituencyLists.length}
+
+ {constituencyLists?.length > 0 && (
+
+ )}
+
+
+ {constituencyLists.map((list) => (
+ {
+ navigate(
+ SignatureCollectionPaths.ParliamentaryConstituencyList.replace(
+ ':constituencyName',
+ constituencyName,
+ ).replace(':listId', list.id),
+ )
+ },
+ }}
+ tag={{
+ label: 'Cancel collection',
+ renderTag: () => (
+
+
+
+
+
+ }
+ onConfirm={() => {
+ //onCancelCollection(list.id)
+ }}
+ buttonTextConfirm={formatMessage(
+ m.cancelCollectionModalConfirmButton,
+ )}
+ buttonPropsConfirm={{
+ variant: 'primary',
+ colorScheme: 'destructive',
+ }}
+ buttonTextCancel={formatMessage(
+ m.cancelCollectionModalCancelButton,
+ )}
+ />
+ ),
+ }}
+ />
+ ))}
+
+
+
+
+
+
+ )
+}
+
+export default Constituency
diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/DownloadReports/index.tsx b/libs/portals/admin/signature-collection/src/screens-parliamentary/DownloadReports/index.tsx
new file mode 100644
index 000000000000..d01d8442eb30
--- /dev/null
+++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/DownloadReports/index.tsx
@@ -0,0 +1,62 @@
+import { useLocale } from '@island.is/localization'
+import { ActionCard, Box, Button, Stack, Text } from '@island.is/island-ui/core'
+import { useState } from 'react'
+import { Modal } from '@island.is/react/components'
+import { SignatureCollectionArea } from '@island.is/api/schema'
+import { m } from '../../lib/messages'
+
+export const DownloadReports = ({
+ areas,
+}: {
+ areas: SignatureCollectionArea[]
+}) => {
+ const { formatMessage } = useLocale()
+ const [modalDownloadReportsIsOpen, setModalDownloadReportsIsOpen] =
+ useState(false)
+
+ return (
+
+
+ setModalDownloadReportsIsOpen(false)}
+ closeButtonLabel={''}
+ >
+ {formatMessage(m.downloadReportsDescription)}
+
+
+ {areas.map((area) => (
+ {
+ console.log('download')
+ },
+ }}
+ />
+ ))}
+
+
+
+
+ )
+}
+
+export default DownloadReports
diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/List/index.tsx b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/index.tsx
new file mode 100644
index 000000000000..1f40fa8e7ed7
--- /dev/null
+++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/index.tsx
@@ -0,0 +1,77 @@
+import {
+ Box,
+ Breadcrumbs,
+ GridColumn,
+ GridContainer,
+ GridRow,
+} from '@island.is/island-ui/core'
+import { useLocale } from '@island.is/localization'
+import { IntroHeader, PortalNavigation } from '@island.is/portals/core'
+import { signatureCollectionNavigation } from '../../lib/navigation'
+import { m, parliamentaryMessages } from '../../lib/messages'
+import { useLoaderData } from 'react-router-dom'
+import { SignatureCollectionList } from '@island.is/api/schema'
+import { PaperSignees } from './paperSignees'
+import { SignatureCollectionPaths } from '../../lib/paths'
+import ActionExtendDeadline from '../../shared-components/extendDeadline'
+import Signees from '../../shared-components/signees'
+import ActionReviewComplete from '../../shared-components/completeReview'
+
+const List = () => {
+ const { formatMessage } = useLocale()
+ const { list } = useLoaderData() as {
+ list: SignatureCollectionList
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default List
diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/identityAndCanSignLookup.graphql b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/identityAndCanSignLookup.graphql
new file mode 100644
index 000000000000..373c3e542c71
--- /dev/null
+++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/identityAndCanSignLookup.graphql
@@ -0,0 +1,11 @@
+query Identity($input: IdentityInput!) {
+ identity(input: $input) {
+ nationalId
+ type
+ name
+ }
+}
+
+query CanSign($input: SignatureCollectionCanSignFromPaperInput!) {
+ signatureCollectionCanSignFromPaper(input: $input)
+}
diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/index.tsx b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/index.tsx
new file mode 100644
index 000000000000..083cb10e2b7b
--- /dev/null
+++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/index.tsx
@@ -0,0 +1,188 @@
+import {
+ Box,
+ Text,
+ Button,
+ GridRow,
+ GridColumn,
+ GridContainer,
+ AlertMessage,
+ Input,
+} from '@island.is/island-ui/core'
+import { useLocale, useNamespaces } from '@island.is/localization'
+import * as nationalId from 'kennitala'
+import { useEffect, useState } from 'react'
+import { InputController } from '@island.is/shared/form-fields'
+import { useForm } from 'react-hook-form'
+import { toast } from 'react-toastify'
+import { m } from '../../../lib/messages'
+import {
+ useCanSignQuery,
+ useIdentityQuery,
+} from './identityAndCanSignLookup.generated'
+import { useSignatureCollectionUploadPaperSignatureMutation } from './uploadPaperSignee.generated'
+
+export const PaperSignees = ({ listId }: { listId: string }) => {
+ useNamespaces('sp.signatureCollection')
+ const { formatMessage } = useLocale()
+ const { control, reset } = useForm()
+
+ const [nationalIdInput, setNationalIdInput] = useState('')
+ const [nationalIdTypo, setNationalIdTypo] = useState(false)
+ const [page, setPage] = useState('')
+ const [name, setName] = useState('')
+
+ const { data, loading } = useIdentityQuery({
+ variables: { input: { nationalId: nationalIdInput } },
+ skip: nationalIdInput.length !== 10 || !nationalId.isValid(nationalIdInput),
+ onCompleted: (data) => setName(data.identity?.name || ''),
+ })
+
+ const { data: canSign, loading: loadingCanSign } = useCanSignQuery({
+ variables: {
+ input: {
+ signeeNationalId: nationalIdInput,
+ listId,
+ },
+ },
+ })
+
+ useEffect(() => {
+ if (nationalIdInput.length === 10) {
+ setNationalIdTypo(
+ !nationalId.isValid(nationalIdInput) ||
+ (!loading && !data?.identity?.name),
+ )
+ } else {
+ setName('')
+ setNationalIdTypo(false)
+ }
+ }, [nationalIdInput, loading, data])
+
+ const [uploadPaperSignee, { loading: uploadingPaperSignature }] =
+ useSignatureCollectionUploadPaperSignatureMutation({
+ variables: {
+ input: {
+ listId: listId,
+ nationalId: nationalIdInput,
+ pageNumber: Number(page),
+ },
+ },
+ onCompleted: () => {
+ toast.success(formatMessage(m.paperSigneeSuccess))
+ reset()
+ setNationalIdTypo(false)
+ setName('')
+ },
+ onError: () => {
+ toast.error(formatMessage(m.paperSigneeError))
+ },
+ })
+
+ const onClearForm = () => {
+ reset() // resets nationalId field
+ setNationalIdTypo(false)
+ setName('')
+ }
+
+ return (
+
+
+
+ {formatMessage(m.paperSigneesHeader)}
+
+
+
+
+
+
+
+
+
+
+ {
+ setNationalIdInput(e.target.value.replace(/\W/g, ''))
+ }}
+ error={nationalIdTypo ? ' ' : undefined}
+ loading={loading || loadingCanSign}
+ icon={name && canSign ? 'checkmark' : undefined}
+ />
+
+
+ setPage(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {nationalIdTypo && (
+
+
+
+ )}
+ {name && !loadingCanSign && !canSign && (
+
+
+
+ )}
+
+ )
+}
diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/uploadPaperSignee.graphql b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/uploadPaperSignee.graphql
new file mode 100644
index 000000000000..d06c5c01131f
--- /dev/null
+++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/List/paperSignees/uploadPaperSignee.graphql
@@ -0,0 +1,8 @@
+mutation SignatureCollectionUploadPaperSignature(
+ $input: SignatureCollectionUploadPaperSignatureInput!
+) {
+ signatureCollectionUploadPaperSignature(input: $input) {
+ success
+ reasons
+ }
+}
diff --git a/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx b/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx
new file mode 100644
index 000000000000..c422b85c122b
--- /dev/null
+++ b/libs/portals/admin/signature-collection/src/screens-parliamentary/index.tsx
@@ -0,0 +1,109 @@
+import {
+ ActionCard,
+ FilterInput,
+ GridColumn,
+ GridContainer,
+ GridRow,
+ Stack,
+ Box,
+ Breadcrumbs,
+ Text,
+} from '@island.is/island-ui/core'
+import { useLocale } from '@island.is/localization'
+import { IntroHeader, PortalNavigation } from '@island.is/portals/core'
+import { signatureCollectionNavigation } from '../lib/navigation'
+import { m, parliamentaryMessages } from '../lib/messages'
+import { useLoaderData, useNavigate } from 'react-router-dom'
+import { SignatureCollectionPaths } from '../lib/paths'
+import CompareLists from '../shared-components/compareLists'
+import { ListsLoaderReturn } from '../loaders/AllLists.loader'
+import DownloadReports from './DownloadReports'
+
+const ParliamentaryRoot = () => {
+ const { formatMessage } = useLocale()
+
+ const navigate = useNavigate()
+ const { collection, allLists } = useLoaderData() as ListsLoaderReturn
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ console.log('search')}
+ placeholder={formatMessage(m.searchInListPlaceholder)}
+ backgroundColor="blue"
+ />
+
+
+
+
+ {collection?.areas.map((area) => (
+ l.area.name === area.name).length
+ }
+ heading={area.name}
+ cta={{
+ label: formatMessage(m.viewConstituency),
+ variant: 'text',
+ onClick: () => {
+ navigate(
+ SignatureCollectionPaths.ParliamentaryConstituency.replace(
+ ':constituencyName',
+ area.name,
+ ),
+ )
+ },
+ }}
+ />
+ ))}
+
+
+
+
+
+ )
+}
+
+export default ParliamentaryRoot
diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/components/completeCollectionProcessing/finishCollectionProcess.graphql b/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/completeCollectionProcessing/finishCollectionProcess.graphql
similarity index 100%
rename from libs/portals/admin/signature-collection/src/screens/AllLists/components/completeCollectionProcessing/finishCollectionProcess.graphql
rename to libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/completeCollectionProcessing/finishCollectionProcess.graphql
diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/components/completeCollectionProcessing/index.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/completeCollectionProcessing/index.tsx
similarity index 100%
rename from libs/portals/admin/signature-collection/src/screens/AllLists/components/completeCollectionProcessing/index.tsx
rename to libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/completeCollectionProcessing/index.tsx
diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/components/reviewCandidates/index.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/reviewCandidates/index.tsx
similarity index 100%
rename from libs/portals/admin/signature-collection/src/screens/AllLists/components/reviewCandidates/index.tsx
rename to libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/reviewCandidates/index.tsx
diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/components/reviewCandidates/removeCandidate.graphql b/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/reviewCandidates/removeCandidate.graphql
similarity index 100%
rename from libs/portals/admin/signature-collection/src/screens/AllLists/components/reviewCandidates/removeCandidate.graphql
rename to libs/portals/admin/signature-collection/src/screens-presidential/AllLists/components/reviewCandidates/removeCandidate.graphql
diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/index.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/index.tsx
similarity index 94%
rename from libs/portals/admin/signature-collection/src/screens/AllLists/index.tsx
rename to libs/portals/admin/signature-collection/src/screens-presidential/AllLists/index.tsx
index 624dadff74a1..cdf864934e62 100644
--- a/libs/portals/admin/signature-collection/src/screens/AllLists/index.tsx
+++ b/libs/portals/admin/signature-collection/src/screens-presidential/AllLists/index.tsx
@@ -26,16 +26,16 @@ import {
countryAreas,
pageSize,
} from '../../lib/utils'
-import CompareLists from './components/compareLists'
import { format as formatNationalId } from 'kennitala'
-import CreateCollection from './components/createCollection'
import electionsCommitteeLogo from '../../../assets/electionsCommittee.svg'
import nationalRegistryLogo from '../../../assets/nationalRegistry.svg'
import ActionCompleteCollectionProcessing from './components/completeCollectionProcessing'
import ListInfo from '../List/components/listInfoAlert'
-import { ListsLoaderReturn } from './AllLists.loader'
-import EmptyState from './components/emptyState'
+import EmptyState from '../../shared-components/emptyState'
import ReviewCandidates from './components/reviewCandidates'
+import CompareLists from '../../shared-components/compareLists'
+import { ListsLoaderReturn } from '../../loaders/AllLists.loader'
+import CreateCollection from '../../shared-components/createCollection'
const Lists = ({ allowedToProcess }: { allowedToProcess: boolean }) => {
const { formatMessage } = useLocale()
@@ -125,7 +125,7 @@ const Lists = ({ allowedToProcess }: { allowedToProcess: boolean }) => {
span={['12/12', '12/12', '12/12', '8/12']}
>
{
- {lists?.length > 0 ? (
+ {lists?.length > 0 && collection.isPresidential ? (
<>
{filters.input.length > 0 ||
@@ -282,8 +282,8 @@ const Lists = ({ allowedToProcess }: { allowedToProcess: boolean }) => {
icon: 'arrowForward',
onClick: () => {
navigate(
- SignatureCollectionPaths.SignatureList.replace(
- ':id',
+ SignatureCollectionPaths.PresidentialList.replace(
+ ':listId',
list.id,
),
)
@@ -311,7 +311,7 @@ const Lists = ({ allowedToProcess }: { allowedToProcess: boolean }) => {
/>
)}
- {lists?.length > 0 && (
+ {lists?.length > 0 && collection.isPresidential && (
{
)}
- {lists?.length > 0 && (
+ {lists?.length > 0 && collection.isPresidential && (
)}
diff --git a/libs/portals/admin/signature-collection/src/screens/List/components/listInfoAlert/index.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/List/components/listInfoAlert/index.tsx
similarity index 100%
rename from libs/portals/admin/signature-collection/src/screens/List/components/listInfoAlert/index.tsx
rename to libs/portals/admin/signature-collection/src/screens-presidential/List/components/listInfoAlert/index.tsx
diff --git a/libs/portals/admin/signature-collection/src/screens/List/components/paperUpload/index.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/List/components/paperUpload/index.tsx
similarity index 100%
rename from libs/portals/admin/signature-collection/src/screens/List/components/paperUpload/index.tsx
rename to libs/portals/admin/signature-collection/src/screens-presidential/List/components/paperUpload/index.tsx
diff --git a/libs/portals/admin/signature-collection/src/screens/List/components/paperUpload/paperUpload.graphql b/libs/portals/admin/signature-collection/src/screens-presidential/List/components/paperUpload/paperUpload.graphql
similarity index 100%
rename from libs/portals/admin/signature-collection/src/screens/List/components/paperUpload/paperUpload.graphql
rename to libs/portals/admin/signature-collection/src/screens-presidential/List/components/paperUpload/paperUpload.graphql
diff --git a/libs/portals/admin/signature-collection/src/screens/List/components/skeleton.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/List/components/skeleton.tsx
similarity index 100%
rename from libs/portals/admin/signature-collection/src/screens/List/components/skeleton.tsx
rename to libs/portals/admin/signature-collection/src/screens-presidential/List/components/skeleton.tsx
diff --git a/libs/portals/admin/signature-collection/src/screens/List/index.tsx b/libs/portals/admin/signature-collection/src/screens-presidential/List/index.tsx
similarity index 95%
rename from libs/portals/admin/signature-collection/src/screens/List/index.tsx
rename to libs/portals/admin/signature-collection/src/screens-presidential/List/index.tsx
index 4610b450ad2d..89d3aca6dc65 100644
--- a/libs/portals/admin/signature-collection/src/screens/List/index.tsx
+++ b/libs/portals/admin/signature-collection/src/screens-presidential/List/index.tsx
@@ -11,15 +11,15 @@ import {
GridRow,
Text,
} from '@island.is/island-ui/core'
-import Signees from './components/signees'
-import ActionExtendDeadline from './components/extendDeadline'
-import ActionReviewComplete from './components/completeReview'
import PaperUpload from './components/paperUpload'
import ListInfo from './components/listInfoAlert'
import electionsCommitteeLogo from '../../../assets/electionsCommittee.svg'
import nationalRegistryLogo from '../../../assets/nationalRegistry.svg'
import { format as formatNationalId } from 'kennitala'
import { ListStatus } from '../../lib/utils'
+import ActionReviewComplete from '../../shared-components/completeReview'
+import Signees from '../../shared-components/signees'
+import ActionExtendDeadline from '../../shared-components/extendDeadline'
export const List = ({ allowedToProcess }: { allowedToProcess: boolean }) => {
const { list, listStatus } = useLoaderData() as {
diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/components/compareLists/compareLists.graphql b/libs/portals/admin/signature-collection/src/shared-components/compareLists/compareLists.graphql
similarity index 100%
rename from libs/portals/admin/signature-collection/src/screens/AllLists/components/compareLists/compareLists.graphql
rename to libs/portals/admin/signature-collection/src/shared-components/compareLists/compareLists.graphql
diff --git a/libs/portals/admin/signature-collection/src/screens/AllLists/components/compareLists/index.tsx b/libs/portals/admin/signature-collection/src/shared-components/compareLists/index.tsx
similarity index 93%
rename from libs/portals/admin/signature-collection/src/screens/AllLists/components/compareLists/index.tsx
rename to libs/portals/admin/signature-collection/src/shared-components/compareLists/index.tsx
index 9ede275c65a1..4dd05e5091d1 100644
--- a/libs/portals/admin/signature-collection/src/screens/AllLists/components/compareLists/index.tsx
+++ b/libs/portals/admin/signature-collection/src/shared-components/compareLists/index.tsx
@@ -8,15 +8,15 @@ import {
toast,
} from '@island.is/island-ui/core'
import { useLocale } from '@island.is/localization'
-import { m } from '../../../../lib/messages'
import { useState } from 'react'
import { Modal } from '@island.is/react/components'
import { useBulkCompareMutation } from './compareLists.generated'
import { format as formatNationalId } from 'kennitala'
import { SignatureCollectionSignature } from '@island.is/api/schema'
-import { createFileList, getFileData } from '../../../../lib/utils'
import { Skeleton } from './skeleton'
import { useUnsignAdminMutation } from './removeSignatureFromList.generated'
+import { m } from '../../lib/messages'
+import { createFileList, getFileData } from '../../lib/utils'
const CompareLists = ({ collectionId }: { collectionId: string }) => {
const { formatMessage } = useLocale()
@@ -57,7 +57,7 @@ const CompareLists = ({ collectionId }: { collectionId: string }) => {
},
})
- if (res.data && res.data.signatureCollectionAdminUnsign.success) {
+ if (res.data?.signatureCollectionAdminUnsign.success) {
toast.success(formatMessage(m.unsignFromListSuccess))
setUploadResults(
uploadResults?.filter((result: SignatureCollectionSignature) => {
@@ -84,20 +84,20 @@ const CompareLists = ({ collectionId }: { collectionId: string }) => {
return (
-
+
{formatMessage(m.compareListsDescription)}
- ) : !isExpired ? (
+ ) : !isExpired && href ? (
{formatMessage(coreMessages.buttonEdit)}
- ) : (
+ ) : href ? (
{formatMessage(coreMessages.buttonRenew)}
- )}
+ ) : null}
)}
diff --git a/libs/portals/shared-modules/delegations/src/components/delegations/incoming/DelegationsIncoming.tsx b/libs/portals/shared-modules/delegations/src/components/delegations/incoming/DelegationsIncoming.tsx
index c444a298339b..526439fcdbb4 100644
--- a/libs/portals/shared-modules/delegations/src/components/delegations/incoming/DelegationsIncoming.tsx
+++ b/libs/portals/shared-modules/delegations/src/components/delegations/incoming/DelegationsIncoming.tsx
@@ -14,6 +14,7 @@ import { DelegationsEmptyState } from '../DelegationsEmptyState'
import { DelegationIncomingModal } from './DelegationIncomingModal/DelegationIncomingModal'
import { useAuthDelegationsIncomingQuery } from './DelegationIncomingModal/DelegationIncomingModal.generated'
import { AuthCustomDelegationIncoming } from '../../../types/customDelegation'
+import { DelegationPaths } from '../../../lib/paths'
export const DelegationsIncoming = () => {
const { formatMessage, lang = 'is' } = useLocale()
@@ -78,6 +79,7 @@ export const DelegationsIncoming = () => {
}}
direction="incoming"
variant="incoming"
+ href={`${DelegationPaths.Delegations}/${delegation.id}`}
/>
),
)}
diff --git a/libs/portals/shared-modules/delegations/src/components/delegations/outgoing/DelegationsOutgoing.tsx b/libs/portals/shared-modules/delegations/src/components/delegations/outgoing/DelegationsOutgoing.tsx
index dc7f9174dee8..b4b079342a28 100644
--- a/libs/portals/shared-modules/delegations/src/components/delegations/outgoing/DelegationsOutgoing.tsx
+++ b/libs/portals/shared-modules/delegations/src/components/delegations/outgoing/DelegationsOutgoing.tsx
@@ -17,6 +17,7 @@ import { useAuthDelegationsOutgoingQuery } from './DelegationsOutgoing.generated
import { AuthCustomDelegationOutgoing } from '../../../types/customDelegation'
import { ALL_DOMAINS } from '../../../constants/domain'
import { m } from '../../../lib/messages'
+import { DelegationPaths } from '../../../lib/paths'
const prepareDomainName = (domainName: string | null) =>
domainName === ALL_DOMAINS ? null : domainName
@@ -114,6 +115,7 @@ export const DelegationsOutgoing = () => {
)
}}
variant="outgoing"
+ href={`${DelegationPaths.Delegations}/${delegation.id}`}
/>
),
)}
diff --git a/libs/service-portal/core/src/components/TabNavigation/TabNavigation.tsx b/libs/service-portal/core/src/components/TabNavigation/TabNavigation.tsx
index 6a371f092197..a08bfe67c73d 100644
--- a/libs/service-portal/core/src/components/TabNavigation/TabNavigation.tsx
+++ b/libs/service-portal/core/src/components/TabNavigation/TabNavigation.tsx
@@ -16,6 +16,7 @@ import { useWindowSize } from 'react-use'
import InstitutionPanel from '../InstitutionPanel/InstitutionPanel'
import * as styles from './TabNavigation.css'
import { TabBar } from './TabBar'
+import { TabNavigationInstitutionPanel } from './TabNavigationInstitutionPanel'
interface Props {
pathname?: string
@@ -50,14 +51,10 @@ export const TabNavigation: React.FC = ({ items, pathname, label }) => {
navigate(id)
}
}
-
- const { data: organization, loading } = useOrganization(
- activePath?.activeChild?.serviceProvider ?? activePath.serviceProvider,
- )
-
+ const serviceProvider =
+ activePath?.activeChild?.serviceProvider ?? activePath.serviceProvider
const descriptionText =
activePath.activeChild?.description ?? activePath?.description
-
const tooltipText =
activePath.activeChild?.serviceProviderTooltip ??
activePath.serviceProviderTooltip
@@ -157,23 +154,13 @@ export const TabNavigation: React.FC = ({ items, pathname, label }) => {
)}
- {(activePath.displayServiceProviderLogo ||
- activePath?.displayServiceProviderLogo) &&
+ {activePath?.displayServiceProviderLogo &&
+ serviceProvider &&
!isMobile && (
-
- {organization?.logo && (
-
- )}
-
+
)}
diff --git a/libs/service-portal/core/src/components/TabNavigation/TabNavigationInstitutionPanel.tsx b/libs/service-portal/core/src/components/TabNavigation/TabNavigationInstitutionPanel.tsx
new file mode 100644
index 000000000000..343849cc1431
--- /dev/null
+++ b/libs/service-portal/core/src/components/TabNavigation/TabNavigationInstitutionPanel.tsx
@@ -0,0 +1,39 @@
+import { GridColumn } from '@island.is/island-ui/core'
+import { useOrganization } from '@island.is/service-portal/graphql'
+import InstitutionPanel from '../InstitutionPanel/InstitutionPanel'
+import { MessageDescriptor } from 'react-intl'
+import { OrganizationSlugType } from '@island.is/shared/constants'
+import { useLocale } from '@island.is/localization'
+import { useWindowSize } from 'react-use'
+import { theme } from '@island.is/island-ui/theme'
+
+interface Props {
+ serviceProvider: OrganizationSlugType
+ tooltipText?: MessageDescriptor
+}
+
+export const TabNavigationInstitutionPanel = ({
+ tooltipText,
+ serviceProvider,
+}: Props) => {
+ const { formatMessage } = useLocale()
+ const { data: organization, loading } = useOrganization(serviceProvider)
+ const { width } = useWindowSize()
+
+ const isMobile = width < theme.breakpoints.md
+
+ return (
+
+ {organization?.logo && (
+
+ )}
+
+ )
+}
diff --git a/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.graphql b/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.graphql
index e445153ee094..d120a592b167 100644
--- a/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.graphql
+++ b/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.graphql
@@ -33,6 +33,12 @@ query getOrgansList($locale: String) {
}
}
-mutation updateOrganDonationInfo($input: HealthDirectorateOrganDonorInput!) {
- healthDirectorateOrganDonationUpdateDonorStatus(input: $input)
+mutation updateOrganDonationInfo(
+ $input: HealthDirectorateOrganDonorInput!
+ $locale: String
+) {
+ healthDirectorateOrganDonationUpdateDonorStatus(
+ input: $input
+ locale: $locale
+ )
}
diff --git a/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.tsx b/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.tsx
index ba3501ea7d22..4f6d6e6ff7d1 100644
--- a/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.tsx
+++ b/libs/service-portal/health/src/screens/OrganDonation/OrganDonation.tsx
@@ -30,6 +30,12 @@ const OrganDonation = () => {
: formatMessage(m.iAmOrganDonorText)
: formatMessage(m.iAmNotOrganDonorText)
+ const heading = donorStatus?.isDonor
+ ? donorStatus.limitations?.hasLimitations
+ ? formatMessage(m.iAmOrganDonorWithExceptions)
+ : formatMessage(m.iAmOrganDonor)
+ : formatMessage(m.iAmNotOrganDonor)
+
return (
{
{formatMessage(m.takeOnOrganDonation)}
{
isDonor: radioValue === OPT_IN || radioValue === OPT_IN_EXCEPTIONS,
organLimitations: radioValue === OPT_IN_EXCEPTIONS ? limitations : [],
},
+ locale: lang,
},
})
}
diff --git a/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts b/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts
index 45d50fdbe81c..a9903bf9edb8 100644
--- a/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts
+++ b/libs/service-portal/signature-collection/src/hooks/graphql/queries.ts
@@ -159,7 +159,16 @@ export const GetCurrentCollection = gql`
`
export const GetCanSign = gql`
- query Query($input: SignatureCollectionCanSignInput!) {
- signatureCollectionCanSign(input: $input)
+ query Query($input: SignatureCollectionCanSignFromPaperInput!) {
+ signatureCollectionCanSignFromPaper(input: $input)
+ }
+`
+
+export const GetCollectors = gql`
+ query SignatureCollectionCollectors {
+ signatureCollectionCollectors {
+ nationalId
+ name
+ }
}
`
diff --git a/libs/service-portal/signature-collection/src/hooks/index.ts b/libs/service-portal/signature-collection/src/hooks/index.ts
index 9c3800136b8e..5dc621198d87 100644
--- a/libs/service-portal/signature-collection/src/hooks/index.ts
+++ b/libs/service-portal/signature-collection/src/hooks/index.ts
@@ -8,6 +8,7 @@ import {
GetListsForOwner,
GetCurrentCollection,
GetCanSign,
+ GetCollectors,
} from './graphql/queries'
import {
SignatureCollectionListBase,
@@ -16,6 +17,7 @@ import {
SignatureCollectionSuccess,
SignatureCollection,
SignatureCollectionSignedList,
+ SignatureCollectionCollector,
} from '@island.is/api/schema'
export const useGetSignatureList = (listId: string) => {
@@ -149,18 +151,32 @@ export const useGetCurrentCollection = () => {
}
}
-export const useGetCanSign = (signeeId: string, isValidId: boolean) => {
+export const useGetCanSign = (
+ signeeId: string,
+ listId: string,
+ isValidId: boolean,
+) => {
const { data: getCanSignData, loading: loadingCanSign } = useQuery(
GetCanSign,
{
variables: {
input: {
signeeNationalId: signeeId,
+ listId: listId,
},
},
skip: !signeeId || signeeId.length !== 10 || !isValidId,
},
)
- const canSign = getCanSignData?.signatureCollectionCanSign ?? false
+ const canSign = getCanSignData?.signatureCollectionCanSignFromPaper ?? false
return { canSign, loadingCanSign }
}
+
+export const useGetCollectors = () => {
+ const { data: getCollectorsData, loading: loadingCollectors } =
+ useQuery(GetCollectors)
+ const collectors =
+ (getCollectorsData?.signatureCollectionCollectors as SignatureCollectionCollector[]) ??
+ []
+ return { collectors, loadingCollectors }
+}
diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx
index 877cf5b5bd8f..6589d6d8ce3b 100644
--- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx
+++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/PaperSignees.tsx
@@ -44,6 +44,7 @@ export const PaperSignees = ({
})
const { canSign, loadingCanSign } = useGetCanSign(
nationalIdInput,
+ listId,
nationalId.isValid(nationalIdInput),
)
@@ -120,6 +121,7 @@ export const PaperSignees = ({
name="nationalId"
label={formatMessage(m.signeeNationalId)}
format="######-####"
+ required
defaultValue={nationalIdInput}
onChange={(e) => {
setNationalIdInput(e.target.value.replace(/\W/g, ''))
@@ -134,6 +136,7 @@ export const PaperSignees = ({
id="page"
name="page"
type="number"
+ required
label={formatMessage(m.paperNumber)}
value={page}
onChange={(e) => setPage(e.target.value)}
diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/index.tsx
index 581ffb77b6be..5b5aebf5875b 100644
--- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/index.tsx
+++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/ViewList/Signees/index.tsx
@@ -98,13 +98,15 @@ const Signees = () => {
{!s.isDigital && (
-
-
+
{s.pageNumber}
+
+
+
)}
diff --git a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx
index 659882231e88..541665057dbc 100644
--- a/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx
+++ b/libs/service-portal/signature-collection/src/screens/Parliamentary/OwnerView/index.tsx
@@ -20,12 +20,16 @@ import {
SignatureCollectionList,
SignatureCollectionSuccess,
} from '@island.is/api/schema'
-import { OwnerParliamentarySkeleton } from '../../../skeletons'
-import { useGetListsForOwner } from '../../../hooks'
+import {
+ CollectorSkeleton,
+ OwnerParliamentarySkeleton,
+} from '../../../skeletons'
+import { useGetCollectors, useGetListsForOwner } from '../../../hooks'
import { SignatureCollection } from '@island.is/api/schema'
import { useMutation } from '@apollo/client'
import { cancelCollectionMutation } from '../../../hooks/graphql/mutations'
import copyToClipboard from 'copy-to-clipboard'
+import { formatNationalId } from '@island.is/portals/core'
const OwnerView = ({
currentCollection,
@@ -36,6 +40,7 @@ const OwnerView = ({
const { formatMessage } = useLocale()
const { listsForOwner, loadingOwnerLists, refetchListsForOwner } =
useGetListsForOwner(currentCollection?.id || '')
+ const { collectors, loadingCollectors } = useGetCollectors()
const [cancelCollection] = useMutation(
cancelCollectionMutation,
@@ -84,7 +89,7 @@ const OwnerView = ({
)}
{loadingOwnerLists ? (
-
+
) : (
@@ -179,10 +184,23 @@ const OwnerView = ({
-
- {'Nafni Nafnason'}
- {'010130-3019'}
-
+ {loadingCollectors ? (
+
+
+
+
+
+
+
+
+ ) : (
+ collectors.map((collector) => (
+
+ {collector.name}
+ {formatNationalId(collector.nationalId)}
+
+ ))
+ )}
diff --git a/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx b/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx
index df7b95016cf0..8d3790d1f9fc 100644
--- a/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx
+++ b/libs/service-portal/signature-collection/src/screens/shared/SignedList/index.tsx
@@ -21,6 +21,9 @@ const SignedList = ({
useNamespaces('sp.signatureCollection')
const { formatMessage } = useLocale()
const [modalIsOpen, setModalIsOpen] = useState(false)
+ const [listIdToUnsign, setListIdToUnsign] = useState(
+ undefined,
+ )
// SignedList is typically singular, although it may consist of multiple entries, which in that case will all be invalid
const { signedLists, loadingSignedLists, refetchSignedLists } =
@@ -29,10 +32,7 @@ const SignedList = ({
const [unSign, { loading }] = useMutation(unSignList, {
variables: {
input: {
- listId:
- signedLists && signedLists?.length === 1
- ? signedLists[0].id
- : undefined,
+ listId: listIdToUnsign,
},
},
})
@@ -85,7 +85,10 @@ const SignedList = ({
variant: 'text',
colorScheme: 'destructive',
},
- onClick: () => setModalIsOpen(true),
+ onClick: () => {
+ setListIdToUnsign(list.id)
+ setModalIsOpen(true)
+ },
icon: undefined,
}
: undefined
@@ -120,41 +123,44 @@ const SignedList = ({
: undefined
}
/>
- setModalIsOpen(false)}
- >
-
-
- {formatMessage(m.unSignList)}
-
-
- {formatMessage(m.unSignModalMessage)}
-
-
- {
- onUnSignList()
- }}
- >
- {formatMessage(m.unSignModalConfirmButton)}
-
-
-
-
)
})}
+ {
+ setListIdToUnsign(undefined)
+ setModalIsOpen(false)
+ }}
+ >
+
+
+ {formatMessage(m.unSignList)}
+
+
+ {formatMessage(m.unSignModalMessage)}
+
+
+ {
+ onUnSignList()
+ }}
+ >
+ {formatMessage(m.unSignModalConfirmButton)}
+
+
+
+
)}
diff --git a/libs/service-portal/signature-collection/src/skeletons.tsx b/libs/service-portal/signature-collection/src/skeletons.tsx
index 04982b35c517..3b1ab3291fa3 100644
--- a/libs/service-portal/signature-collection/src/skeletons.tsx
+++ b/libs/service-portal/signature-collection/src/skeletons.tsx
@@ -21,3 +21,7 @@ export const SkeletonTable = () => {
)
}
+
+export const CollectorSkeleton = () => {
+ return
+}
diff --git a/libs/services/auth/testing/src/fixtures/fixture-factory.ts b/libs/services/auth/testing/src/fixtures/fixture-factory.ts
index a8929a9a9f13..b6a37fbf32b5 100644
--- a/libs/services/auth/testing/src/fixtures/fixture-factory.ts
+++ b/libs/services/auth/testing/src/fixtures/fixture-factory.ts
@@ -376,6 +376,7 @@ export class FixtureFactory {
domainName,
fromName,
scopes = [],
+ referenceId,
}: CreateCustomDelegation): Promise {
const delegation = await this.get(Delegation).create({
id: faker.datatype.uuid(),
@@ -384,6 +385,7 @@ export class FixtureFactory {
domainName,
fromDisplayName: fromName ?? faker.name.findName(),
toName: faker.name.findName(),
+ referenceId: referenceId ?? undefined,
})
delegation.delegationScopes = await Promise.all(
diff --git a/libs/services/auth/testing/src/fixtures/types.ts b/libs/services/auth/testing/src/fixtures/types.ts
index c3f77d51825b..8b96406dd1e3 100644
--- a/libs/services/auth/testing/src/fixtures/types.ts
+++ b/libs/services/auth/testing/src/fixtures/types.ts
@@ -35,8 +35,11 @@ export type CreateCustomDelegationScope = Optional<
'validFrom' | 'validTo'
>
export type CreateCustomDelegation = Optional<
- Pick,
- 'toNationalId' | 'fromNationalId' | 'fromName'
+ Pick<
+ DelegationDTO,
+ 'toNationalId' | 'fromNationalId' | 'fromName' | 'referenceId'
+ >,
+ 'toNationalId' | 'fromNationalId' | 'fromName' | 'referenceId'
> & {
domainName: string
scopes?: CreateCustomDelegationScope[]
diff --git a/libs/shared/connected/src/lib/SignatureLists/SignatureLists.tsx b/libs/shared/connected/src/lib/SignatureLists/SignatureLists.tsx
index b042eddc70e0..fe0d8a9cb905 100644
--- a/libs/shared/connected/src/lib/SignatureLists/SignatureLists.tsx
+++ b/libs/shared/connected/src/lib/SignatureLists/SignatureLists.tsx
@@ -115,7 +115,11 @@ export const SignatureLists: FC<
size: 'small',
onClick: () =>
window.open(
- `${window.location.origin}/umsoknir/maela-med-frambodi/?candidate=${candidate.id}`,
+ `${window.location.origin}/umsoknir/${
+ collection.isPresidential
+ ? 'maela-med-frambodi'
+ : 'maela-med-althingisframbodi'
+ }/?candidate=${candidate.id}`,
'_blank',
),
}