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 ecf113cd4174..968d52a64cd3 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 @@ -177,7 +177,32 @@ export class FileController { ) } - @Get(['subpoena/:defendantId', 'subpoena/:defendantId/:subpoenaId']) + @Get('serviceCertificate/:defendantId/:subpoenaId') + @Header('Content-Type', 'application/pdf') + getServiceCertificatePdf( + @Param('id') id: string, + @Param('defendantId') defendantId: string, + @CurrentHttpUser() user: User, + @Req() req: Request, + @Res() res: Response, + @Param('subpoenaId') subpoenaId?: string, + ): Promise { + this.logger.debug( + `Getting service certificate for defendant ${defendantId} of case ${id} as a pdf document`, + ) + + return this.fileService.tryGetFile( + user.id, + AuditedAction.GET_SERVICE_CERTIFICATE_PDF, + id, + `defendant/${defendantId}/subpoena/${subpoenaId}/serviceCertificate`, + req, + res, + 'pdf', + ) + } + + @Get('subpoena/:defendantId') @Header('Content-Type', 'application/pdf') getSubpoenaPdf( @Param('id') id: string, 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 cfc4f9447db8..c434dd53bb6e 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,7 +6,6 @@ import { Header, Inject, Param, - Query, Req, Res, UseGuards, @@ -20,7 +19,7 @@ import { CurrentHttpUser, JwtInjectBearerAuthGuard, } from '@island.is/judicial-system/auth' -import type { SubpoenaType, User } from '@island.is/judicial-system/types' +import type { User } from '@island.is/judicial-system/types' import { FileService } from './file.service' @@ -201,6 +200,31 @@ export class LimitedAccessFileController { ) } + @Get('serviceCertificate/:defendantId/:subpoenaId') + @Header('Content-Type', 'application/pdf') + getServiceCertificatePdf( + @Param('id') id: string, + @Param('defendantId') defendantId: string, + @CurrentHttpUser() user: User, + @Req() req: Request, + @Res() res: Response, + @Param('subpoenaId') subpoenaId?: string, + ): Promise { + this.logger.debug( + `Getting service certificate for defendant ${defendantId} of case ${id} as a pdf document`, + ) + + return this.fileService.tryGetFile( + user.id, + AuditedAction.GET_SERVICE_CERTIFICATE_PDF, + id, + `limitedAccess/defendant/${defendantId}/subpoena/${subpoenaId}/serviceCertificate`, + req, + res, + 'pdf', + ) + } + @Get('allFiles') @Header('Content-Type', 'application/zip') async getAllFiles( diff --git a/apps/judicial-system/backend/src/app/formatters/index.ts b/apps/judicial-system/backend/src/app/formatters/index.ts index ea9b36ec95ba..48f8af67b53e 100644 --- a/apps/judicial-system/backend/src/app/formatters/index.ts +++ b/apps/judicial-system/backend/src/app/formatters/index.ts @@ -36,3 +36,4 @@ export { createCaseFilesRecord } from './caseFilesRecordPdf' export { createIndictment } from './indictmentPdf' export { createConfirmedPdf } from './confirmedPdf' export { createSubpoena } from './subpoenaPdf' +export { createServiceCertificate } from './serviceCertificatePdf' diff --git a/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts b/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts index 4f44aa249e93..da5fec6a809e 100644 --- a/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts +++ b/apps/judicial-system/backend/src/app/formatters/pdfHelpers.ts @@ -470,6 +470,14 @@ export const addMediumText = ( addText(doc, mediumFontSize, text, font) } +export const addMediumCenteredText = ( + doc: PDFKit.PDFDocument, + text: string, + font?: string, +) => { + addAlignedText(doc, mediumFontSize, text, 'center', font) +} + export const addNormalPlusText = ( doc: PDFKit.PDFDocument, text: string, diff --git a/apps/judicial-system/backend/src/app/formatters/serviceCertificatePdf.ts b/apps/judicial-system/backend/src/app/formatters/serviceCertificatePdf.ts new file mode 100644 index 000000000000..571c3cfa8ae9 --- /dev/null +++ b/apps/judicial-system/backend/src/app/formatters/serviceCertificatePdf.ts @@ -0,0 +1,166 @@ +import PDFDocument from 'pdfkit' + +import { FormatMessage } from '@island.is/cms-translations' + +import { + capitalize, + formatDate, + formatDOB, + getWordByGender, + Word, +} from '@island.is/judicial-system/formatters' +import { ServiceStatus, SubpoenaType } from '@island.is/judicial-system/types' + +import { serviceCertificate as strings } from '../messages' +import { Case } from '../modules/case' +import { Defendant } from '../modules/defendant' +import { Subpoena } from '../modules/subpoena' +import { + addEmptyLines, + addFooter, + addHugeHeading, + addMediumCenteredText, + addNormalCenteredText, + addNormalText, + setTitle, +} from './pdfHelpers' + +const getSubpoenaType = (subpoenaType?: SubpoenaType): string => { + switch (subpoenaType) { + case SubpoenaType.ABSENCE: + return 'Útivistarfyrirkall' + case SubpoenaType.ARREST: + return 'Handtökufyrirkall' + default: + // Should never happen + return 'Ekki skráð' + } +} + +export const createServiceCertificate = ( + theCase: Case, + defendant: Defendant, + subpoena: Subpoena, + formatMessage: FormatMessage, +): Promise => { + const doc = new PDFDocument({ + size: 'A4', + margins: { + top: 40, + bottom: 60, + left: 50, + right: 50, + }, + bufferPages: true, + }) + + const sinc: Buffer[] = [] + + doc.on('data', (chunk) => sinc.push(chunk)) + + setTitle(doc, formatMessage(strings.title)) + + addHugeHeading(doc, formatMessage(strings.title).toUpperCase(), 'Times-Bold') + addMediumCenteredText( + doc, + `Mál nr. ${theCase.courtCaseNumber || ''}`, + 'Times-Bold', + ) + addNormalCenteredText(doc, theCase.court?.name || '', 'Times-Bold') + + addEmptyLines(doc, 2) + + addMediumCenteredText( + doc, + `Birting tókst ${ + subpoena.serviceDate ? formatDate(subpoena.serviceDate, 'PPp') : '' + }`, + 'Times-Bold', + ) + + addEmptyLines(doc) + + addNormalText(doc, 'Birtingaraðili: ', 'Times-Bold', true) + addNormalText( + doc, + subpoena.serviceStatus === ServiceStatus.ELECTRONICALLY + ? 'Rafrænt pósthólf island.is' + : subpoena.servedBy || '', + 'Times-Roman', + ) + + if (subpoena.serviceStatus !== ServiceStatus.ELECTRONICALLY) { + addNormalText(doc, 'Athugasemd: ', 'Times-Bold', true) + addNormalText( + doc, + subpoena.serviceStatus === ServiceStatus.DEFENDER + ? `Birt fyrir verjanda ${ + defendant.defenderName ? `- ${defendant.defenderName}` : '' + }` + : subpoena.comment || '', + 'Times-Roman', + ) + } + + addEmptyLines(doc, 2) + + addNormalText( + doc, + `${capitalize(getWordByGender(Word.AKAERDI, defendant.gender))}: `, + 'Times-Bold', + true, + ) + addNormalText( + doc, + defendant.name && defendant.nationalId && defendant.address + ? `${defendant.name}, ${formatDOB( + defendant.nationalId, + defendant.noNationalId, + )}, ${defendant.address}` + : 'Ekki skráður', + 'Times-Roman', + ) + + addEmptyLines(doc, 2) + + addNormalText(doc, 'Ákærandi: ', 'Times-Bold', true) + addNormalText( + doc, + theCase.prosecutor?.institution + ? theCase.prosecutor.institution.name + : 'Ekki skráður', + 'Times-Roman', + ) + + addNormalText(doc, 'Dómari: ', 'Times-Bold', true) + addNormalText( + doc, + theCase.judge ? theCase.judge.name : 'Ekki skráður', + 'Times-Roman', + ) + + addEmptyLines(doc) + + addNormalText(doc, 'Þingfesting: ', 'Times-Bold', true) + addNormalText( + doc, + formatDate( + subpoena.arraignmentDate ? new Date(subpoena.arraignmentDate) : null, + 'Pp', + ) || 'Ekki skráð', + 'Times-Roman', + ) + + addNormalText(doc, 'Staður: ', 'Times-Bold', true) + addNormalText(doc, subpoena.location || 'Ekki skráður', 'Times-Roman') + + addNormalText(doc, 'Tegund fyrirkalls: ', 'Times-Bold', true) + addNormalText(doc, getSubpoenaType(defendant.subpoenaType), 'Times-Roman') + + addFooter(doc) + doc.end() + + return new Promise((resolve) => + doc.on('end', () => resolve(Buffer.concat(sinc))), + ) +} diff --git a/apps/judicial-system/backend/src/app/messages/index.ts b/apps/judicial-system/backend/src/app/messages/index.ts index 7c74f43383d6..96b48e4a337a 100644 --- a/apps/judicial-system/backend/src/app/messages/index.ts +++ b/apps/judicial-system/backend/src/app/messages/index.ts @@ -8,3 +8,4 @@ export { courtUpload } from './courtUpload' export { caseFilesRecord } from './pdfCaseFilesRecord' export { indictment } from './pdfIndictment' export { subpoena } from './pdfSubpoena' +export { serviceCertificate } from './pdfServiceCertificate' diff --git a/apps/judicial-system/backend/src/app/messages/pdfServiceCertificate.ts b/apps/judicial-system/backend/src/app/messages/pdfServiceCertificate.ts new file mode 100644 index 000000000000..aa12add09623 --- /dev/null +++ b/apps/judicial-system/backend/src/app/messages/pdfServiceCertificate.ts @@ -0,0 +1,9 @@ +import { defineMessage } from '@formatjs/intl' + +export const serviceCertificate = { + title: defineMessage({ + id: 'judicial.system.backend:pdf.service_certificate.title', + defaultMessage: 'Birtingarvottorð', + description: 'Notaður sem titill á birtingarvottorði.', + }), +} diff --git a/apps/judicial-system/backend/src/app/modules/case/guards/rolesRules.ts b/apps/judicial-system/backend/src/app/modules/case/guards/rolesRules.ts index dbb92e3e7b11..6835cdae1f85 100644 --- a/apps/judicial-system/backend/src/app/modules/case/guards/rolesRules.ts +++ b/apps/judicial-system/backend/src/app/modules/case/guards/rolesRules.ts @@ -283,7 +283,7 @@ export const defenderGeneratedPdfRule: RolesRule = { const user: User = request.user const theCase: Case = request.case - // Deny if something is missing - shuould never happen + // Deny if something is missing - should never happen if (!user || !theCase) { return false } diff --git a/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts b/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts index 860356035526..721c79cbf394 100644 --- a/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/pdf.service.ts @@ -26,6 +26,7 @@ import { Confirmation, createCaseFilesRecord, createIndictment, + createServiceCertificate, createSubpoena, getCourtRecordPdfAsBuffer, getCustodyNoticePdfAsBuffer, @@ -355,4 +356,21 @@ export class PdfService { return generatedPdf } + + async getServiceCertificatePdf( + theCase: Case, + defendant: Defendant, + subpoena: Subpoena, + ): Promise { + await this.refreshFormatMessage() + + const generatedPdf = await createServiceCertificate( + theCase, + defendant, + subpoena, + this.formatMessage, + ) + + return generatedPdf + } } diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/limitedAccessSubpoena.controller.ts b/apps/judicial-system/backend/src/app/modules/subpoena/limitedAccessSubpoena.controller.ts index b805d15ba33e..655359ada19b 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/limitedAccessSubpoena.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/limitedAccessSubpoena.controller.ts @@ -32,7 +32,10 @@ import { } from '../case' import { CurrentDefendant, Defendant, DefendantExistsGuard } from '../defendant' import { CurrentSubpoena } from './guards/subpoena.decorator' -import { SubpoenaExistsOptionalGuard } from './guards/subpoenaExists.guard' +import { + SubpoenaExistsGuard, + SubpoenaExistsOptionalGuard, +} from './guards/subpoenaExists.guard' import { Subpoena } from './models/subpoena.model' @Controller([ @@ -45,7 +48,6 @@ import { Subpoena } from './models/subpoena.model' new CaseTypeGuard(indictmentCases), CaseReadGuard, DefendantExistsGuard, - SubpoenaExistsOptionalGuard, ) @ApiTags('limited access subpoenas') export class LimitedAccessSubpoenaController { @@ -54,6 +56,7 @@ export class LimitedAccessSubpoenaController { @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} + @UseGuards(SubpoenaExistsOptionalGuard) @RolesRules(defenderGeneratedPdfRule) @Get() @Header('Content-Type', 'application/pdf') @@ -82,4 +85,35 @@ export class LimitedAccessSubpoenaController { res.end(pdf) } + + @UseGuards(SubpoenaExistsGuard) + @RolesRules(defenderGeneratedPdfRule) + @Get('serviceCertificate') + @Header('Content-Type', 'application/pdf') + @ApiOkResponse({ + content: { 'application/pdf': {} }, + description: + 'Gets the service certificate for a given defendant as a pdf document', + }) + async getServiceCertificatePdf( + @Param('caseId') caseId: string, + @Param('defendantId') defendantId: string, + @Param('subpoenaId') subpoenaId: string, + @CurrentCase() theCase: Case, + @CurrentDefendant() defendant: Defendant, + @CurrentSubpoena() subpoena: Subpoena, + @Res() res: Response, + ): Promise { + this.logger.debug( + `Getting service certificate for defendant ${defendantId} in subpoena ${subpoenaId} of case ${caseId} as a pdf document`, + ) + + const pdf = await this.pdfService.getServiceCertificatePdf( + theCase, + defendant, + subpoena, + ) + + res.end(pdf) + } } diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.controller.ts b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.controller.ts index 0a4eea819bcb..7d8b64a24333 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/subpoena.controller.ts @@ -5,6 +5,7 @@ import { Get, Header, Inject, + InternalServerErrorException, Param, Query, Res, @@ -107,4 +108,45 @@ export class SubpoenaController { res.end(pdf) } + + @RolesRules( + prosecutorRule, + prosecutorRepresentativeRule, + publicProsecutorStaffRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + districtCourtAssistantRule, + ) + @Get('serviceCertificate') + @Header('Content-Type', 'application/pdf') + @ApiOkResponse({ + content: { 'application/pdf': {} }, + description: + 'Gets the service certificate for a given defendant as a pdf document', + }) + async getServiceCertificatePdf( + @Param('caseId') caseId: string, + @Param('defendantId') defendantId: string, + @Param('subpoenaId') subpoenaId: string, + @CurrentCase() theCase: Case, + @CurrentDefendant() defendant: Defendant, + @Res() res: Response, + @CurrentSubpoena() subpoena?: Subpoena, + ): Promise { + this.logger.debug( + `Getting service certificate for defendant ${defendantId} in subpoena ${subpoenaId} of case ${caseId} as a pdf document`, + ) + + if (!subpoena) { + throw new InternalServerErrorException('Missing subpoena') + } + + const pdf = await this.pdfService.getServiceCertificatePdf( + theCase, + defendant, + subpoena, + ) + + res.end(pdf) + } } diff --git a/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/limitedAccessSubpoenaControllerGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/limitedAccessSubpoenaControllerGuards.spec.ts index 59a5de394aab..3c7ed0f26630 100644 --- a/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/limitedAccessSubpoenaControllerGuards.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/subpoena/test/limitedAccessSubpoenaController/limitedAccessSubpoenaControllerGuards.spec.ts @@ -3,7 +3,6 @@ import { indictmentCases } from '@island.is/judicial-system/types' import { CaseExistsGuard, CaseReadGuard, CaseTypeGuard } from '../../../case' import { DefendantExistsGuard } from '../../../defendant/guards/defendantExists.guard' -import { SubpoenaExistsOptionalGuard } from '../../guards/subpoenaExists.guard' import { LimitedAccessSubpoenaController } from '../../limitedAccessSubpoena.controller' describe('LimitedAccessSubpoenaController - guards', () => { @@ -15,7 +14,7 @@ describe('LimitedAccessSubpoenaController - guards', () => { }) it('should have the right guard configuration', () => { - expect(guards).toHaveLength(7) + expect(guards).toHaveLength(6) expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard) expect(new guards[1]()).toBeInstanceOf(CaseExistsGuard) expect(new guards[2]()).toBeInstanceOf(RolesGuard) @@ -25,6 +24,5 @@ describe('LimitedAccessSubpoenaController - guards', () => { }) expect(new guards[4]()).toBeInstanceOf(CaseReadGuard) expect(new guards[5]()).toBeInstanceOf(DefendantExistsGuard) - expect(new guards[6]()).toBeInstanceOf(SubpoenaExistsOptionalGuard) }) }) 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 a6e7ee8bee11..ab0f020ba135 100644 --- a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.strings.ts +++ b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.strings.ts @@ -43,6 +43,12 @@ export const strings = defineMessages({ id: 'judicial.system.indictments:indictment_case_files_list.subpoena_button_text_v2', defaultMessage: 'Fyrirkall {name} {date}.pdf', description: - 'Notaður sem texti á PDF takka til að sækja firyrkall í ákærum.', + 'Notaður sem texti á PDF takka til að sækja fyrirkall í ákærum.', + }, + serviceCertificateButtonText: { + id: 'judicial.system.indictments:indictment_case_files_list.service_certificate_button_text', + defaultMessage: 'Birtingarvottorð {name}.pdf', + description: + 'Notaður sem texti á PDF takka til að sækja birtingarvottorð í á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 2b2b54f9d9b4..3da0750c5052 100644 --- a/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx +++ b/apps/judicial-system/web/src/components/IndictmentCaseFilesList/IndictmentCaseFilesList.tsx @@ -11,6 +11,7 @@ import { isProsecutionUser, isPublicProsecutor, isPublicProsecutorUser, + isSuccessfulServiceStatus, isTrafficViolationCase, } from '@island.is/judicial-system/types' import { @@ -246,6 +247,17 @@ const IndictmentCaseFilesList: FC = ({ elementId={[defendant.id, subpoena.id]} renderAs="row" /> + {isSuccessfulServiceStatus(subpoena.serviceStatus) && ( + + )} )), )} diff --git a/apps/judicial-system/web/src/components/PdfButton/PdfButton.tsx b/apps/judicial-system/web/src/components/PdfButton/PdfButton.tsx index 987d1fd4a624..b6a2ad2b9815 100644 --- a/apps/judicial-system/web/src/components/PdfButton/PdfButton.tsx +++ b/apps/judicial-system/web/src/components/PdfButton/PdfButton.tsx @@ -18,6 +18,7 @@ interface Props { | 'custodyNotice' | 'indictment' | 'subpoena' + | 'serviceCertificate' disabled?: boolean renderAs?: 'button' | 'row' diff --git a/libs/judicial-system/audit-trail/src/lib/auditTrail.service.ts b/libs/judicial-system/audit-trail/src/lib/auditTrail.service.ts index 2dba14790572..d20af4e3c6c7 100644 --- a/libs/judicial-system/audit-trail/src/lib/auditTrail.service.ts +++ b/libs/judicial-system/audit-trail/src/lib/auditTrail.service.ts @@ -38,6 +38,7 @@ export enum AuditedAction { GET_CUSTODY_NOTICE_PDF = 'GET_CUSTODY_NOTICE_PDF', GET_INDICTMENT_PDF = 'GET_INDICTMENT_PDF', GET_SUBPOENA_PDF = 'GET_SUBPOENA_PDF', + GET_SERVICE_CERTIFICATE_PDF = 'GET_SERVICE_CERTIFICATE', GET_ALL_FILES_ZIP = 'GET_ALL_FILES_ZIP', GET_INSTITUTIONS = 'GET_INSTITUTIONS', GET_SUBPOENA_STATUS = 'GET_SUBPOENA_STATUS', diff --git a/libs/judicial-system/auth/src/lib/guards/roles.guard.ts b/libs/judicial-system/auth/src/lib/guards/roles.guard.ts index 74fb865dba1d..e2305ac4dac6 100644 --- a/libs/judicial-system/auth/src/lib/guards/roles.guard.ts +++ b/libs/judicial-system/auth/src/lib/guards/roles.guard.ts @@ -37,7 +37,6 @@ export class RolesGuard implements CanActivate { if (!rule) { return false } - // Allow if the rule is simply a user role if (typeof rule === 'string') { return true diff --git a/libs/judicial-system/types/src/lib/defendant.ts b/libs/judicial-system/types/src/lib/defendant.ts index a2cb9852d27a..03b843eb6d46 100644 --- a/libs/judicial-system/types/src/lib/defendant.ts +++ b/libs/judicial-system/types/src/lib/defendant.ts @@ -42,7 +42,9 @@ export const successfulServiceStatus: string[] = [ ServiceStatus.IN_PERSON, ] -export const isSuccessfulServiceStatus = (status?: ServiceStatus): boolean => { +export const isSuccessfulServiceStatus = ( + status?: ServiceStatus | null, +): boolean => { return Boolean(status && successfulServiceStatus.includes(status)) }