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 ( + + {title ? {title} : null} + + + ) +} + +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 ( + + {title ? {title} : null} + + + ) +} + +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 && ( - - + + )} - + + + +
{ - if (/^Reglugerð/.test(title)) { - return title.replace(/^Reglugerð/, '') - } - return title -} - -const isGildisTaka = (str: string) => { - return /(öðlast|tekur).*gildi|sett.*með.*(?:heimild|stoð)/.test( - (str || '').toLowerCase(), - ) -} - const formatAffectedAndPlaceAffectedAtEnd = ( groups: { formattedRegBody: HTMLText[] date?: Date | undefined }[], + hideAffected?: boolean, ) => { function formatArray(arr: string[]): string { if (arr.length === 1) { @@ -115,7 +110,10 @@ const formatAffectedAndPlaceAffectedAtEnd = ( }) const uniqueGildistaka = uniq(gildsTakaKeepArray) - const joinedAffected = updatedImpactAffectArray.join('. ') + let joinedAffected = updatedImpactAffectArray.join('. ') + if (hideAffected) { + joinedAffected = '' + } const gildistakaReturn = flatten([...uniqueGildistaka, joinedAffected]).join( '', ) as HTMLText @@ -178,11 +176,11 @@ export const formatAmendingRegBody = ( ) => { const regName = removeRegNamePrefix(name) if (repeal) { - const title = regTitle ? regTitle.replace(/^reglugerð\s*/i, '') + ' ' : '' + const title = regTitle ? regTitle.replace(/^reglugerð\s*/i, '').trim() : '' const text = `

Reglugerð nr. ${regName} ${title.replace( /\.$/, '', - )}fellur brott.

` as HTMLText + )} fellur brott.

` 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)} - + + + { const { formatMessage } = useLocale() @@ -98,9 +98,10 @@ const CreateCollection = ({ collectionId }: { collectionId: string }) => { style={{ minWidth: '150px' }} > + + + + + ) +} + +export default EditPage diff --git a/libs/portals/admin/signature-collection/src/screens/List/components/signees.tsx b/libs/portals/admin/signature-collection/src/shared-components/signees/index.tsx similarity index 82% rename from libs/portals/admin/signature-collection/src/screens/List/components/signees.tsx rename to libs/portals/admin/signature-collection/src/shared-components/signees/index.tsx index 7b8171e68c6e..b69581ddd8e4 100644 --- a/libs/portals/admin/signature-collection/src/screens/List/components/signees.tsx +++ b/libs/portals/admin/signature-collection/src/shared-components/signees/index.tsx @@ -10,13 +10,14 @@ import { } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' import format from 'date-fns/format' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { useLoaderData } from 'react-router-dom' import { format as formatNationalId } from 'kennitala' -import { m } from '../../../lib/messages' import { SignatureCollectionSignature as Signature } from '@island.is/api/schema' -import { pageSize } from '../../../lib/utils' -import SortSignees from './sortSignees' +import SortSignees from '../sortSignees' +import { pageSize } from '../../lib/utils' +import { m } from '../../lib/messages' +import EditPage from './editPage' const Signees = ({ numberOfSignatures }: { numberOfSignatures: number }) => { const { formatMessage } = useLocale() @@ -27,24 +28,21 @@ const Signees = ({ numberOfSignatures }: { numberOfSignatures: number }) => { const [searchTerm, setSearchTerm] = useState('') const [page, setPage] = useState(1) - useEffect(() => { - setSignees(allSignees) - }, [allSignees]) - - useEffect(() => { - let filteredSignees: Signature[] = allSignees - - filteredSignees = filteredSignees.filter((s) => { + const filteredSignees = useMemo(() => { + return allSignees.filter((s) => { + const lowercaseSearchTerm = searchTerm.toLowerCase() return ( - s.signee.name.toLowerCase().includes(searchTerm.toLowerCase()) || + s.signee.name.toLowerCase().includes(lowercaseSearchTerm) || formatNationalId(s.signee.nationalId).includes(searchTerm) || s.signee.nationalId.includes(searchTerm) ) }) + }, [allSignees, searchTerm]) + useEffect(() => { setPage(1) setSignees(filteredSignees) - }, [searchTerm]) + }, [filteredSignees]) return ( @@ -57,7 +55,7 @@ const Signees = ({ numberOfSignatures }: { numberOfSignatures: number }) => { value={searchTerm} onChange={(v) => setSearchTerm(v)} placeholder={formatMessage(m.searchInListPlaceholder)} - backgroundColor="white" + backgroundColor="blue" /> @@ -97,7 +95,6 @@ const Signees = ({ numberOfSignatures }: { numberOfSignatures: number }) => { {formatMessage(m.signeeDate)} {formatMessage(m.signeeName)} {formatMessage(m.signeeNationalId)} - {formatMessage(m.signeeAddress)} @@ -116,20 +113,21 @@ const Signees = ({ numberOfSignatures }: { numberOfSignatures: number }) => { {formatNationalId(s.signee.nationalId)} - - {s.signee.address} - {!s.isDigital && ( {s.pageNumber} - - - + + )} diff --git a/libs/portals/admin/signature-collection/src/screens/List/components/sortSignees/index.tsx b/libs/portals/admin/signature-collection/src/shared-components/sortSignees/index.tsx similarity index 96% rename from libs/portals/admin/signature-collection/src/screens/List/components/sortSignees/index.tsx rename to libs/portals/admin/signature-collection/src/shared-components/sortSignees/index.tsx index d2d1405bfdbe..bd0a16cb1af5 100644 --- a/libs/portals/admin/signature-collection/src/screens/List/components/sortSignees/index.tsx +++ b/libs/portals/admin/signature-collection/src/shared-components/sortSignees/index.tsx @@ -1,7 +1,7 @@ import { useLocale } from '@island.is/localization' import { DropdownMenu } from '@island.is/island-ui/core' -import { m } from '../../../../lib/messages' import { SignatureCollectionSignature as Signature } from '@island.is/api/schema' +import { m } from '../../lib/messages' const SortSignees = ({ signees, diff --git a/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx b/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx index ab3273fb365d..b6f48d880d89 100644 --- a/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx +++ b/libs/portals/shared-modules/delegations/src/components/access/AccessCard.tsx @@ -5,13 +5,13 @@ import * as kennitala from 'kennitala' import { Box, - Text, - Stack, - Tag, - Inline, + Button, Icon, IconMapIcon as IconType, - Button, + Inline, + Stack, + Tag, + Text, Tooltip, } from '@island.is/island-ui/core' import { useLocale } from '@island.is/localization' @@ -20,7 +20,6 @@ import { m as coreMessages } from '@island.is/portals/core' import uniqBy from 'lodash/uniqBy' import sortBy from 'lodash/sortBy' import { m } from '../../lib/messages' -import { DelegationPaths } from '../../lib/paths' import { AuthApiScope, AuthDelegationType } from '@island.is/api/schema' import { AuthCustomDelegation, @@ -57,6 +56,9 @@ interface AccessCardProps { direction?: 'incoming' | 'outgoing' canModify?: boolean + href?: string + + isAdminView?: boolean } export const AccessCard = ({ @@ -66,6 +68,8 @@ export const AccessCard = ({ variant = 'outgoing', direction = 'outgoing', canModify = true, + href, + isAdminView = false, }: AccessCardProps) => { const { formatMessage } = useLocale() const navigate = useNavigate() @@ -74,10 +78,12 @@ export const AccessCard = ({ const hasTags = tags.length > 0 const isOutgoing = variant === 'outgoing' - const href = `${DelegationPaths.Delegations}/${delegation.id}` const isExpired = useMemo(() => { - if (delegation.validTo) { + if ( + delegation.validTo || + delegation.type === AuthDelegationType.GeneralMandate + ) { return isDateExpired(delegation.validTo) } @@ -176,10 +182,12 @@ export const AccessCard = ({ - {!isOutgoing && ( + {(isAdminView || + !isOutgoing || + delegation.type === AuthDelegationType.GeneralMandate) && ( <> {renderDelegationTypeLabel(delegation.type)} - {delegation.domain && ( + {delegation.domain?.name && ( {'|'} @@ -234,7 +242,7 @@ export const AccessCard = ({ {formatMessage(coreMessages.view)} - ) : !isExpired ? ( + ) : !isExpired && href ? ( - ) : ( + ) : href ? ( - )} + ) : 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)} - - - - - - ) })} + { + setListIdToUnsign(undefined) + setModalIsOpen(false) + }} + > + + + {formatMessage(m.unSignList)} + + + {formatMessage(m.unSignModalMessage)} + + + + + + )} 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', ), }