diff --git a/apps/judicial-system/backend/infra/judicial-system-backend.ts b/apps/judicial-system/backend/infra/judicial-system-backend.ts index bab01682da23..8d8fb88fcd95 100644 --- a/apps/judicial-system/backend/infra/judicial-system-backend.ts +++ b/apps/judicial-system/backend/infra/judicial-system-backend.ts @@ -68,6 +68,8 @@ export const serviceSetup = (): ServiceBuilder<'judicial-system-backend'> => EMAIL_FROM_NAME: '/k8s/judicial-system/EMAIL_FROM_NAME', EMAIL_REPLY_TO: '/k8s/judicial-system/EMAIL_REPLY_TO', EMAIL_REPLY_TO_NAME: '/k8s/judicial-system/EMAIL_REPLY_TO_NAME', + POLICE_INSTITUTIONS_EMAILS: + '/k8s/judicial-system/POLICE_INSTITUTIONS_EMAILS', PRISON_EMAIL: '/k8s/judicial-system/PRISON_EMAIL', PRISON_ADMIN_EMAIL: '/k8s/judicial-system/PRISON_ADMIN_EMAIL', PRISON_ADMIN_INDICTMENT_EMAILS: diff --git a/apps/judicial-system/backend/src/app/messages/notifications.ts b/apps/judicial-system/backend/src/app/messages/notifications.ts index a7bf8f898acf..43bda5af9fb3 100644 --- a/apps/judicial-system/backend/src/app/messages/notifications.ts +++ b/apps/judicial-system/backend/src/app/messages/notifications.ts @@ -52,7 +52,7 @@ export const notifications = { }), emailWhitelistDomains: defineMessage({ id: 'judicial.system.backend:notifications.email_whitelist_domains', - defaultMessage: 'omnitrix.is,kolibri.is', + defaultMessage: 'omnitrix.is,kolibri.is,dummy.dd', description: 'Notað til að tilgreina hvort póstfang sé í hvítlista', }), readyForCourt: defineMessages({ diff --git a/apps/judicial-system/backend/src/app/modules/event-log/eventLog.module.ts b/apps/judicial-system/backend/src/app/modules/event-log/eventLog.module.ts index d208d6b47f62..2e23e6aacfa5 100644 --- a/apps/judicial-system/backend/src/app/modules/event-log/eventLog.module.ts +++ b/apps/judicial-system/backend/src/app/modules/event-log/eventLog.module.ts @@ -1,12 +1,17 @@ -import { Module } from '@nestjs/common' +import { forwardRef, Module } from '@nestjs/common' import { SequelizeModule } from '@nestjs/sequelize' +import { MessageModule } from '@island.is/judicial-system/message' + import { EventLog } from './models/eventLog.model' import { EventLogController } from './eventLog.controller' import { EventLogService } from './eventLog.service' @Module({ - imports: [SequelizeModule.forFeature([EventLog])], + imports: [ + forwardRef(() => MessageModule), + SequelizeModule.forFeature([EventLog]), + ], providers: [EventLogService], exports: [EventLogService], controllers: [EventLogController], diff --git a/apps/judicial-system/backend/src/app/modules/event-log/eventLog.service.ts b/apps/judicial-system/backend/src/app/modules/event-log/eventLog.service.ts index c1ee4450f943..52806927363b 100644 --- a/apps/judicial-system/backend/src/app/modules/event-log/eventLog.service.ts +++ b/apps/judicial-system/backend/src/app/modules/event-log/eventLog.service.ts @@ -7,7 +7,11 @@ import { InjectModel } from '@nestjs/sequelize' import type { Logger } from '@island.is/logging' import { LOGGER_PROVIDER } from '@island.is/logging' -import { EventType } from '@island.is/judicial-system/types' +import { MessageService, MessageType } from '@island.is/judicial-system/message' +import { + EventNotificationType, + EventType, +} from '@island.is/judicial-system/types' import { CreateEventLogDto } from './dto/createEventLog.dto' import { EventLog } from './models/eventLog.model' @@ -20,11 +24,19 @@ const allowMultiple: EventType[] = [ EventType.INDICTMENT_CONFIRMED, ] +const eventToNotificationMap: Partial< + Record +> = { + INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR: + EventNotificationType.INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR, +} + @Injectable() export class EventLogService { constructor( @InjectModel(EventLog) private readonly eventLogModel: typeof EventLog, + private readonly messageService: MessageService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -58,6 +70,10 @@ export class EventLogService { // Tolerate failure but log error this.logger.error('Failed to create event log', error) } + + if (caseId) { + this.addEventNotificationToQueue(eventType, caseId) + } } async loginMap( @@ -86,4 +102,23 @@ export class EventLogService { ), ) } + + // Sends events to queue for notification dispatch + private addEventNotificationToQueue(eventType: EventType, caseId: string) { + const notificationType = eventToNotificationMap[eventType] + + if (notificationType) { + try { + this.messageService.sendMessagesToQueue([ + { + type: MessageType.EVENT_NOTIFICATION_DISPATCH, + caseId: caseId, + body: { type: notificationType }, + }, + ]) + } catch (error) { + this.logger.error('Failed to send event notification to queue', error) + } + } + } } diff --git a/apps/judicial-system/backend/src/app/modules/notification/dto/indictmentCaseNotification.dto.ts b/apps/judicial-system/backend/src/app/modules/notification/dto/indictmentCaseNotification.dto.ts new file mode 100644 index 000000000000..201f04c208b5 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/dto/indictmentCaseNotification.dto.ts @@ -0,0 +1,12 @@ +import { IsEnum, IsNotEmpty } from 'class-validator' + +import { ApiProperty } from '@nestjs/swagger' + +import { IndictmentCaseNotificationType } from '@island.is/judicial-system/types' + +export class IndictmentCaseNotificationDto { + @IsNotEmpty() + @IsEnum(IndictmentCaseNotificationType) + @ApiProperty({ enum: IndictmentCaseNotificationType }) + readonly type!: IndictmentCaseNotificationType +} diff --git a/apps/judicial-system/backend/src/app/modules/notification/dto/notificationDispatch.dto.ts b/apps/judicial-system/backend/src/app/modules/notification/dto/notificationDispatch.dto.ts index 017f7a7fde0d..984e356a627a 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/dto/notificationDispatch.dto.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/dto/notificationDispatch.dto.ts @@ -2,7 +2,10 @@ import { IsEnum, IsNotEmpty } from 'class-validator' import { ApiProperty } from '@nestjs/swagger' -import { NotificationDispatchType } from '@island.is/judicial-system/types' +import { + EventNotificationType, + NotificationDispatchType, +} from '@island.is/judicial-system/types' export class NotificationDispatchDto { @IsNotEmpty() @@ -10,3 +13,10 @@ export class NotificationDispatchDto { @ApiProperty({ enum: NotificationDispatchType }) readonly type!: NotificationDispatchType } + +export class EventNotificationDispatchDto { + @IsNotEmpty() + @IsEnum(EventNotificationType) + @ApiProperty({ enum: EventNotificationType }) + readonly type!: EventNotificationType +} diff --git a/apps/judicial-system/backend/src/app/modules/notification/internalNotification.controller.ts b/apps/judicial-system/backend/src/app/modules/notification/internalNotification.controller.ts index 68131739f280..af4c5a235c61 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/internalNotification.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/internalNotification.controller.ts @@ -16,8 +16,9 @@ import { messageEndpoint, MessageType, } from '@island.is/judicial-system/message' +import { indictmentCases } from '@island.is/judicial-system/types' -import { Case, CaseHasExistedGuard, CurrentCase } from '../case' +import { Case, CaseHasExistedGuard, CaseTypeGuard, CurrentCase } from '../case' import { CivilClaimant, CivilClaimantExistsGuard, @@ -30,13 +31,18 @@ import { SubpoenaExistsGuard } from '../subpoena' import { CaseNotificationDto } from './dto/caseNotification.dto' import { CivilClaimantNotificationDto } from './dto/civilClaimantNotification.dto' import { DefendantNotificationDto } from './dto/defendantNotification.dto' +import { IndictmentCaseNotificationDto } from './dto/indictmentCaseNotification.dto' import { InstitutionNotificationDto } from './dto/institutionNotification.dto' -import { NotificationDispatchDto } from './dto/notificationDispatch.dto' +import { + EventNotificationDispatchDto, + NotificationDispatchDto, +} from './dto/notificationDispatch.dto' import { SubpoenaNotificationDto } from './dto/subpoenaNotification.dto' import { DeliverResponse } from './models/deliver.response' import { CaseNotificationService } from './services/caseNotification/caseNotification.service' import { CivilClaimantNotificationService } from './services/civilClaimantNotification/civilClaimantNotification.service' import { DefendantNotificationService } from './services/defendantNotification/defendantNotification.service' +import { IndictmentCaseNotificationService } from './services/indictmentCaseNotification/indictmentCaseNotification.service' import { InstitutionNotificationService } from './services/institutionNotification/institutionNotification.service' import { SubpoenaNotificationService } from './services/subpoenaNotification/subpoenaNotification.service' import { NotificationDispatchService } from './notificationDispatch.service' @@ -52,6 +58,7 @@ export class InternalNotificationController { private readonly subpoenaNotificationService: SubpoenaNotificationService, private readonly defendantNotificationService: DefendantNotificationService, private readonly civilClaimantNotificationService: CivilClaimantNotificationService, + private readonly indictmentCaseNotificationService: IndictmentCaseNotificationService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -77,6 +84,29 @@ export class InternalNotificationController { ) } + @Post( + `case/:caseId/${messageEndpoint[MessageType.INDICTMENT_CASE_NOTIFICATION]}`, + ) + @UseGuards(CaseHasExistedGuard, new CaseTypeGuard(indictmentCases)) + @ApiCreatedResponse({ + type: DeliverResponse, + description: 'Sends a case notification for an existing indictment case', + }) + sendIndictmentCaseNotification( + @Param('caseId') caseId: string, + @CurrentCase() theCase: Case, + @Body() notificationDto: IndictmentCaseNotificationDto, + ): Promise { + this.logger.debug( + `Sending ${notificationDto.type} indictment case notification for case ${caseId}`, + ) + + return this.indictmentCaseNotificationService.sendIndictmentCaseNotification( + notificationDto.type, + theCase, + ) + } + @Post( `case/:caseId/${ messageEndpoint[MessageType.SUBPOENA_NOTIFICATION] @@ -161,34 +191,58 @@ export class InternalNotificationController { ) } - @Post(messageEndpoint[MessageType.NOTIFICATION_DISPATCH]) + @Post(messageEndpoint[MessageType.INSTITUTION_NOTIFICATION]) @ApiCreatedResponse({ type: DeliverResponse, - description: 'Dispatches notifications', + description: 'Sends an institution notification', }) - dispatchNotification( - @Body() notificationDto: NotificationDispatchDto, + sendInstitutionNotification( + @Body() notificationDto: InstitutionNotificationDto, ): Promise { - this.logger.debug(`Dispatching ${notificationDto.type} notification`) + this.logger.debug(`Sending ${notificationDto.type} notification`) - return this.notificationDispatchService.dispatchNotification( + return this.institutionNotificationService.sendNotification( notificationDto.type, + notificationDto.prosecutorsOfficeId, ) } - @Post(messageEndpoint[MessageType.INSTITUTION_NOTIFICATION]) + @Post( + `case/:caseId/${messageEndpoint[MessageType.EVENT_NOTIFICATION_DISPATCH]}`, + ) + @UseGuards(CaseHasExistedGuard) @ApiCreatedResponse({ type: DeliverResponse, - description: 'Sends an institution notification', + description: + 'Dispatches notifications in response to events logged in event log', }) - sendNotification( - @Body() notificationDto: InstitutionNotificationDto, + dispatchEventNotification( + @Param('caseId') caseId: string, + @CurrentCase() theCase: Case, + @Body() notificationDto: EventNotificationDispatchDto, ): Promise { - this.logger.debug(`Sending ${notificationDto.type} notification`) + this.logger.debug( + `Dispatching ${notificationDto.type} event notification for case ${caseId}`, + ) - return this.institutionNotificationService.sendNotification( + return this.notificationDispatchService.dispatchEventNotification( + notificationDto.type, + theCase, + ) + } + + @Post(messageEndpoint[MessageType.NOTIFICATION_DISPATCH]) + @ApiCreatedResponse({ + type: DeliverResponse, + description: 'Dispatches notifications', + }) + dispatchNotification( + @Body() notificationDto: NotificationDispatchDto, + ): Promise { + this.logger.debug(`Dispatching ${notificationDto.type} notification`) + + return this.notificationDispatchService.dispatchNotification( notificationDto.type, - notificationDto.prosecutorsOfficeId, ) } } diff --git a/apps/judicial-system/backend/src/app/modules/notification/notification.config.ts b/apps/judicial-system/backend/src/app/modules/notification/notification.config.ts index aecd5dabb6c9..e91cc08bd354 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/notification.config.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/notification.config.ts @@ -21,6 +21,12 @@ export const notificationModuleConfig = defineConfig({ courtsEmails: env.requiredJSON('COURTS_EMAILS', {}) as { [key: string]: string }, + policeInstitutionEmails: env.requiredJSON( + 'POLICE_INSTITUTIONS_EMAILS', + {}, + ) as { + [key: string]: string + }, }, sms: { courtsMobileNumbers: env.requiredJSON('COURTS_MOBILE_NUMBERS', {}) as { diff --git a/apps/judicial-system/backend/src/app/modules/notification/notification.module.ts b/apps/judicial-system/backend/src/app/modules/notification/notification.module.ts index 101978e3d3ba..47331de9c1c8 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/notification.module.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/notification.module.ts @@ -20,6 +20,7 @@ import { Notification } from './models/notification.model' import { CaseNotificationService } from './services/caseNotification/caseNotification.service' import { CivilClaimantNotificationService } from './services/civilClaimantNotification/civilClaimantNotification.service' import { DefendantNotificationService } from './services/defendantNotification/defendantNotification.service' +import { IndictmentCaseNotificationService } from './services/indictmentCaseNotification/indictmentCaseNotification.service' import { InstitutionNotificationService } from './services/institutionNotification/institutionNotification.service' import { SubpoenaNotificationService } from './services/subpoenaNotification/subpoenaNotification.service' import { InternalNotificationController } from './internalNotification.controller' @@ -47,6 +48,7 @@ import { NotificationDispatchService } from './notificationDispatch.service' CaseNotificationService, CivilClaimantNotificationService, DefendantNotificationService, + IndictmentCaseNotificationService, InstitutionNotificationService, NotificationService, NotificationDispatchService, diff --git a/apps/judicial-system/backend/src/app/modules/notification/notificationDispatch.service.ts b/apps/judicial-system/backend/src/app/modules/notification/notificationDispatch.service.ts index 13884be1e9e0..dbaff1fd065d 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/notificationDispatch.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/notificationDispatch.service.ts @@ -3,16 +3,20 @@ import { Injectable, InternalServerErrorException, } from '@nestjs/common' +import { ConfigType } from '@nestjs/config' import { type Logger, LOGGER_PROVIDER } from '@island.is/logging' import { MessageService, MessageType } from '@island.is/judicial-system/message' import { + EventNotificationType, + IndictmentCaseNotificationType, InstitutionNotificationType, InstitutionType, NotificationDispatchType, } from '@island.is/judicial-system/types' +import { Case } from '../case' import { Institution, InstitutionService } from '../institution' import { DeliverResponse } from './models/deliver.response' @@ -63,4 +67,43 @@ export class NotificationDispatchService { return { delivered: true } } + + private async dispatchIndictmentSentToPublicProsecutorNotifications( + theCase: Case, + ): Promise { + return this.messageService.sendMessagesToQueue([ + { + type: MessageType.INDICTMENT_CASE_NOTIFICATION, + caseId: theCase.id, + body: { + type: IndictmentCaseNotificationType.INDICTMENT_VERDICT_INFO, + }, + }, + ]) + } + + async dispatchEventNotification( + type: EventNotificationType, + theCase: Case, + ): Promise { + try { + switch (type) { + case EventNotificationType.INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR: + await this.dispatchIndictmentSentToPublicProsecutorNotifications( + theCase, + ) + break + default: + throw new InternalServerErrorException( + `Invalid notification type ${type}`, + ) + } + } catch (error) { + this.logger.error('Failed to dispatch event notification', error) + + return { delivered: false } + } + + return { delivered: true } + } } diff --git a/apps/judicial-system/backend/src/app/modules/notification/services/indictmentCaseNotification/indictmentCaseNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/services/indictmentCaseNotification/indictmentCaseNotification.service.ts new file mode 100644 index 000000000000..821662fa0ca0 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/services/indictmentCaseNotification/indictmentCaseNotification.service.ts @@ -0,0 +1,155 @@ +import { + Inject, + Injectable, + InternalServerErrorException, +} from '@nestjs/common' +import { InjectModel } from '@nestjs/sequelize' + +import { IntlService } from '@island.is/cms-translations' +import { EmailService } from '@island.is/email-service' +import { type Logger, LOGGER_PROVIDER } from '@island.is/logging' +import { type ConfigType } from '@island.is/nest/config' + +import { + CaseIndictmentRulingDecision, + IndictmentCaseNotificationType, + IndictmentDecision, +} from '@island.is/judicial-system/types' + +import { Case } from '../../../case' +import { EventService } from '../../../event' +import { BaseNotificationService } from '../../baseNotification.service' +import { DeliverResponse } from '../../models/deliver.response' +import { Notification, Recipient } from '../../models/notification.model' +import { notificationModuleConfig } from '../../notification.config' +import { strings } from './indictmentCaseNotification.strings' + +@Injectable() +export class IndictmentCaseNotificationService extends BaseNotificationService { + constructor( + @InjectModel(Notification) + notificationModel: typeof Notification, + @Inject(notificationModuleConfig.KEY) + config: ConfigType, + @Inject(LOGGER_PROVIDER) logger: Logger, + intlService: IntlService, + emailService: EmailService, + eventService: EventService, + ) { + super( + notificationModel, + emailService, + intlService, + config, + eventService, + logger, + ) + } + + private async sendEmails( + theCase: Case, + notificationType: IndictmentCaseNotificationType, + subject: string, + body: string, + to: { name?: string; email?: string }[], + ) { + const promises: Promise[] = [] + + for (const recipient of to) { + if (recipient.email && recipient.name) { + promises.push( + this.sendEmail( + subject, + body, + recipient.name, + recipient.email, + undefined, + true, + ), + ) + } + } + + const recipients = await Promise.all(promises) + + return this.recordNotification(theCase.id, notificationType, recipients) + } + + private async sendVerdictInfoNotification( + theCase: Case, + ): Promise { + const institutionId = theCase.prosecutor?.institution?.id + const institutionEmail = + (institutionId && + this.config.email.policeInstitutionEmails[institutionId]) ?? + undefined + + const hasRuling = + theCase.indictmentRulingDecision === CaseIndictmentRulingDecision.RULING + + if (!institutionEmail || !hasRuling) { + // institution does not want to receive these emails or the case does not have a ruling + return { delivered: true } + } + + const formattedSubject = this.formatMessage( + strings.indictmentCompletedWithRuling.subject, + { + courtCaseNumber: theCase.courtCaseNumber, + }, + ) + + const formattedBody = this.formatMessage( + strings.indictmentCompletedWithRuling.body, + { + courtCaseNumber: theCase.courtCaseNumber, + courtName: theCase.court?.name, + serviceRequirement: + theCase.defendants && theCase.defendants[0].serviceRequirement, + caseOrigin: theCase.origin, + }, + ) + + return this.sendEmails( + theCase, + IndictmentCaseNotificationType.INDICTMENT_VERDICT_INFO, + formattedSubject, + formattedBody, + [ + { + name: theCase.prosecutor?.institution?.name, + email: institutionEmail, + }, + ], + ) + } + + private sendNotification( + notificationType: IndictmentCaseNotificationType, + theCase: Case, + ): Promise { + switch (notificationType) { + case IndictmentCaseNotificationType.INDICTMENT_VERDICT_INFO: + return this.sendVerdictInfoNotification(theCase) + + default: + throw new InternalServerErrorException( + `Invalid indictment notification type: ${notificationType}`, + ) + } + } + + async sendIndictmentCaseNotification( + type: IndictmentCaseNotificationType, + theCase: Case, + ): Promise { + await this.refreshFormatMessage() + try { + return await this.sendNotification(type, theCase) + } catch (error) { + this.logger.error('Failed to send indictment case notification', error) + + return { delivered: false } + } + } +} diff --git a/apps/judicial-system/backend/src/app/modules/notification/services/indictmentCaseNotification/indictmentCaseNotification.strings.ts b/apps/judicial-system/backend/src/app/modules/notification/services/indictmentCaseNotification/indictmentCaseNotification.strings.ts new file mode 100644 index 000000000000..c9ed5ec52513 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/services/indictmentCaseNotification/indictmentCaseNotification.strings.ts @@ -0,0 +1,19 @@ +import { defineMessages } from '@formatjs/intl' + +export const strings = { + indictmentCompletedWithRuling: defineMessages({ + subject: { + id: 'judicial.system.backend:indictment_case_notifications.verdict_service.subject', + defaultMessage: 'Máli lokið {courtCaseNumber}', + description: + 'Notað sem titill í tilkynningu um stöðu birtingar dóms í lokinni ákæru', + }, + body: { + id: 'judicial.system.backend:indictment_case_notifications.verdict_service.body', + defaultMessage: + 'Máli {courtCaseNumber} hjá {courtName} hefur verið lokið.\n\nNiðurstaða: Dómur\n\n{serviceRequirement, select, REQUIRED {Birta skal dómfellda dóminn} NOT_REQUIRED {Birting dóms ekki þörf} NOT_APPLICABLE {Dómfelldi var viðstaddur dómsuppkvaðningu} other {}}\n\n{caseOrigin, select, LOKE {Dómur er aðgengilegur í LÖKE.} other {}}', + description: + 'Notað sem body í tilkynningu um stöðu birtingar dóms í lokinni ákæru', + }, + }), +} diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts b/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts index c5ea4ac34521..fa41ef517272 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/createTestingNotificationModule.ts @@ -33,6 +33,7 @@ import { NotificationDispatchService } from '../notificationDispatch.service' import { CaseNotificationService } from '../services/caseNotification/caseNotification.service' import { CivilClaimantNotificationService } from '../services/civilClaimantNotification/civilClaimantNotification.service' import { DefendantNotificationService } from '../services/defendantNotification/defendantNotification.service' +import { IndictmentCaseNotificationService } from '../services/indictmentCaseNotification/indictmentCaseNotification.service' import { InstitutionNotificationService } from '../services/institutionNotification/institutionNotification.service' jest.mock('@island.is/judicial-system/message') @@ -130,6 +131,7 @@ export const createTestingNotificationModule = async () => { InstitutionNotificationService, DefendantNotificationService, CivilClaimantNotificationService, + IndictmentCaseNotificationService, ], }) .useMocker((token) => { @@ -158,6 +160,9 @@ export const createTestingNotificationModule = async () => { internalNotificationController: notificationModule.get( InternalNotificationController, ), + indictmentCaseNotificationService: notificationModule.get( + IndictmentCaseNotificationService, + ), } notificationModule.close() diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/eventNotificationDispatch/eventNotificationDispatch.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/eventNotificationDispatch/eventNotificationDispatch.spec.ts new file mode 100644 index 000000000000..490b135e8429 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/eventNotificationDispatch/eventNotificationDispatch.spec.ts @@ -0,0 +1,77 @@ +import { uuid } from 'uuidv4' + +import { MessageService, MessageType } from '@island.is/judicial-system/message' +import { + EventNotificationType, + IndictmentCaseNotificationType, +} from '@island.is/judicial-system/types' + +import { createTestingNotificationModule } from '../../createTestingNotificationModule' + +import { Case } from '../../../../case' +import { InternalNotificationController } from '../../../internalNotification.controller' + +describe('InternalNotificationController - Dispatch event notifications', () => { + const theCase = { id: uuid() } as Case + let mockMessageService: MessageService + let internalNotificationController: InternalNotificationController + + beforeEach(async () => { + const { messageService, internalNotificationController: controller } = + await createTestingNotificationModule() + + mockMessageService = messageService + internalNotificationController = controller + + const mockSendMessagesToQueue = + messageService.sendMessagesToQueue as jest.Mock + mockSendMessagesToQueue.mockResolvedValueOnce(undefined) + }) + + const notificationScenarios = [ + { + notificationType: + EventNotificationType.INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR, + expectedMessage: { + type: MessageType.INDICTMENT_CASE_NOTIFICATION, + caseId: theCase.id, + body: { + type: IndictmentCaseNotificationType.INDICTMENT_VERDICT_INFO, + }, + }, + }, + ] + + it.each( + notificationScenarios.map(({ notificationType, expectedMessage }) => ({ + notificationType, + expectedMessage, + description: `should send message to queue for notification type ${notificationType}`, + })), + )('$description', async ({ notificationType, expectedMessage }) => { + const result = + await internalNotificationController.dispatchEventNotification( + theCase.id, + theCase, + { type: notificationType }, + ) + + expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledWith([ + expectedMessage, + ]) + expect(result).toEqual({ delivered: true }) + }) + + it('will fail if a new EventNotificationType is missing from the tests', () => { + const allNotificationTypes = Object.values(EventNotificationType) + const testedNotificationTypes = notificationScenarios.map( + (scenario) => scenario.notificationType, + ) + + const missingNotificationTypes = allNotificationTypes.filter( + (type) => !testedNotificationTypes.includes(type), + ) + + expect(missingNotificationTypes).toEqual([]) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/indictmentCaseNotification/sendIndictmentVerdictInfoNotification.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/indictmentCaseNotification/sendIndictmentVerdictInfoNotification.spec.ts new file mode 100644 index 000000000000..3c4af29a137b --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/indictmentCaseNotification/sendIndictmentVerdictInfoNotification.spec.ts @@ -0,0 +1,146 @@ +import { uuid } from 'uuidv4' + +import { EmailService } from '@island.is/email-service' + +import { + CaseIndictmentRulingDecision, + CaseOrigin, + IndictmentCaseNotificationType, + ServiceRequirement, +} from '@island.is/judicial-system/types' + +import { + createTestingNotificationModule, + createTestUsers, +} from '../../createTestingNotificationModule' + +import { Case } from '../../../../case' +import { DeliverResponse } from '../../../models/deliver.response' + +interface Then { + result: DeliverResponse + error: Error +} + +type GivenWhenThen = ( + theCase: Case, + notificationType: IndictmentCaseNotificationType, +) => Promise + +describe('IndictmentCaseService', () => { + const { prosecutorsOffice, defender } = createTestUsers([ + 'prosecutorsOffice', + 'defender', + ]) + const caseId = uuid() + const courtName = uuid() + const prosecutorsOfficeName = prosecutorsOffice.name + const prosecutorsOfficeEmail = prosecutorsOffice.email + const prosecutorInstitutionId = uuid() + const courtCaseNumber = uuid() + let theCase = { + id: caseId, + court: { name: courtName }, + origin: CaseOrigin.LOKE, + defendants: [ + { + defenderNationalId: defender.nationalId, + defenderName: defender.name, + defenderEmail: defender.email, + serviceRequirement: ServiceRequirement.REQUIRED, + }, + ], + prosecutor: { + institution: { name: prosecutorsOfficeName, id: prosecutorInstitutionId }, + }, + + courtCaseNumber, + } as Case + + let mockEmailService: EmailService + let givenWhenThen: GivenWhenThen + + process.env.POLICE_INSTITUTIONS_EMAILS = `{"${prosecutorInstitutionId}": "${prosecutorsOfficeEmail}"}` + + beforeEach(async () => { + const { emailService, indictmentCaseNotificationService } = + await createTestingNotificationModule() + + mockEmailService = emailService + + givenWhenThen = async ( + theCase: Case, + notificationType: IndictmentCaseNotificationType, + ) => { + const then = {} as Then + + await indictmentCaseNotificationService + .sendIndictmentCaseNotification(notificationType, theCase) + .then((result) => (then.result = result)) + .catch((error) => (then.error = error)) + + return then + } + }) + + describe('notifications sent to institution with registered e-mail', () => { + it('should not send a notification if indictment ruling decision is not RULING', async () => { + const then = await givenWhenThen( + theCase, + IndictmentCaseNotificationType.INDICTMENT_VERDICT_INFO, + ) + + expect(mockEmailService.sendEmail).toBeCalledTimes(0) + expect(then.result.delivered).toEqual(true) + }) + + it('should send a notification when indictment ruling decision is RULING', async () => { + const caseWithRulingDecision = { + ...theCase, + indictmentRulingDecision: CaseIndictmentRulingDecision.RULING, + } as Case + + const then = await givenWhenThen( + caseWithRulingDecision, + IndictmentCaseNotificationType.INDICTMENT_VERDICT_INFO, + ) + + expect(mockEmailService.sendEmail).toBeCalledWith( + expect.objectContaining({ + to: [ + { address: prosecutorsOfficeEmail, name: prosecutorsOfficeName }, + ], + subject: expect.stringContaining(`Máli lokið ${courtCaseNumber}`), + html: expect.stringContaining( + `Máli ${courtCaseNumber} hjá ${courtName} hefur verið lokið.`, + ), + }), + ) + + expect(then.result).toEqual({ delivered: true }) + }) + }) + + describe('notifications sent to institution without registered e-mail', () => { + it('should not send a notification', async () => { + const invalidProsecutorInstitutionId = uuid() + theCase = { + ...theCase, + prosecutor: { + ...theCase.prosecutor, + institution: { + ...theCase.prosecutor?.institution, + id: invalidProsecutorInstitutionId, + }, + }, + } as Case + + await givenWhenThen( + theCase, + IndictmentCaseNotificationType.INDICTMENT_VERDICT_INFO, + ) + + expect(mockEmailService.sendEmail).not.toBeCalled() + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentsWaitingForConfirmationNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentsWaitingForConfirmationNotifications.spec.ts index 3c20d0d96a26..86c82c0c7275 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentsWaitingForConfirmationNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendIndictmentsWaitingForConfirmationNotifications.spec.ts @@ -60,7 +60,7 @@ describe('InternalNotificationController - Send indictments waiting for confirma const then = {} as Then await internalNotificationController - .sendNotification({ + .sendInstitutionNotification({ type: InstitutionNotificationType.INDICTMENTS_WAITING_FOR_CONFIRMATION, prosecutorsOfficeId, }) diff --git a/charts/judicial-system/values.dev.yaml b/charts/judicial-system/values.dev.yaml index dd58423affdc..c5d0f700d69b 100644 --- a/charts/judicial-system/values.dev.yaml +++ b/charts/judicial-system/values.dev.yaml @@ -225,6 +225,7 @@ judicial-system-backend: NOVA_PASSWORD: '/k8s/judicial-system/NOVA_PASSWORD' NOVA_URL: '/k8s/judicial-system/NOVA_URL' NOVA_USERNAME: '/k8s/judicial-system/NOVA_USERNAME' + POLICE_INSTITUTIONS_EMAILS: '/k8s/judicial-system/POLICE_INSTITUTIONS_EMAILS' PRISON_ADMIN_EMAIL: '/k8s/judicial-system/PRISON_ADMIN_EMAIL' PRISON_ADMIN_INDICTMENT_EMAILS: '/k8s/judicial-system/PRISON_ADMIN_INDICTMENT_EMAILS' PRISON_EMAIL: '/k8s/judicial-system/PRISON_EMAIL' diff --git a/charts/judicial-system/values.prod.yaml b/charts/judicial-system/values.prod.yaml index ffcef752f5ec..e0747b1254c4 100644 --- a/charts/judicial-system/values.prod.yaml +++ b/charts/judicial-system/values.prod.yaml @@ -225,6 +225,7 @@ judicial-system-backend: NOVA_PASSWORD: '/k8s/judicial-system/NOVA_PASSWORD' NOVA_URL: '/k8s/judicial-system/NOVA_URL' NOVA_USERNAME: '/k8s/judicial-system/NOVA_USERNAME' + POLICE_INSTITUTIONS_EMAILS: '/k8s/judicial-system/POLICE_INSTITUTIONS_EMAILS' PRISON_ADMIN_EMAIL: '/k8s/judicial-system/PRISON_ADMIN_EMAIL' PRISON_ADMIN_INDICTMENT_EMAILS: '/k8s/judicial-system/PRISON_ADMIN_INDICTMENT_EMAILS' PRISON_EMAIL: '/k8s/judicial-system/PRISON_EMAIL' diff --git a/charts/judicial-system/values.staging.yaml b/charts/judicial-system/values.staging.yaml index 30afa9a56e74..e6b543a61ce6 100644 --- a/charts/judicial-system/values.staging.yaml +++ b/charts/judicial-system/values.staging.yaml @@ -225,6 +225,7 @@ judicial-system-backend: NOVA_PASSWORD: '/k8s/judicial-system/NOVA_PASSWORD' NOVA_URL: '/k8s/judicial-system/NOVA_URL' NOVA_USERNAME: '/k8s/judicial-system/NOVA_USERNAME' + POLICE_INSTITUTIONS_EMAILS: '/k8s/judicial-system/POLICE_INSTITUTIONS_EMAILS' PRISON_ADMIN_EMAIL: '/k8s/judicial-system/PRISON_ADMIN_EMAIL' PRISON_ADMIN_INDICTMENT_EMAILS: '/k8s/judicial-system/PRISON_ADMIN_INDICTMENT_EMAILS' PRISON_EMAIL: '/k8s/judicial-system/PRISON_EMAIL' diff --git a/charts/services/judicial-system-backend/values.dev.yaml b/charts/services/judicial-system-backend/values.dev.yaml index 8aeb2656041e..df2ab994c0bc 100644 --- a/charts/services/judicial-system-backend/values.dev.yaml +++ b/charts/services/judicial-system-backend/values.dev.yaml @@ -138,6 +138,7 @@ secrets: NOVA_PASSWORD: '/k8s/judicial-system/NOVA_PASSWORD' NOVA_URL: '/k8s/judicial-system/NOVA_URL' NOVA_USERNAME: '/k8s/judicial-system/NOVA_USERNAME' + POLICE_INSTITUTIONS_EMAILS: '/k8s/judicial-system/POLICE_INSTITUTIONS_EMAILS' PRISON_ADMIN_EMAIL: '/k8s/judicial-system/PRISON_ADMIN_EMAIL' PRISON_ADMIN_INDICTMENT_EMAILS: '/k8s/judicial-system/PRISON_ADMIN_INDICTMENT_EMAILS' PRISON_EMAIL: '/k8s/judicial-system/PRISON_EMAIL' diff --git a/charts/services/judicial-system-backend/values.prod.yaml b/charts/services/judicial-system-backend/values.prod.yaml index 2807d821b9d9..d0047fef3de8 100644 --- a/charts/services/judicial-system-backend/values.prod.yaml +++ b/charts/services/judicial-system-backend/values.prod.yaml @@ -138,6 +138,7 @@ secrets: NOVA_PASSWORD: '/k8s/judicial-system/NOVA_PASSWORD' NOVA_URL: '/k8s/judicial-system/NOVA_URL' NOVA_USERNAME: '/k8s/judicial-system/NOVA_USERNAME' + POLICE_INSTITUTIONS_EMAILS: '/k8s/judicial-system/POLICE_INSTITUTIONS_EMAILS' PRISON_ADMIN_EMAIL: '/k8s/judicial-system/PRISON_ADMIN_EMAIL' PRISON_ADMIN_INDICTMENT_EMAILS: '/k8s/judicial-system/PRISON_ADMIN_INDICTMENT_EMAILS' PRISON_EMAIL: '/k8s/judicial-system/PRISON_EMAIL' diff --git a/charts/services/judicial-system-backend/values.staging.yaml b/charts/services/judicial-system-backend/values.staging.yaml index 7221f2acfd51..30787a83c52d 100644 --- a/charts/services/judicial-system-backend/values.staging.yaml +++ b/charts/services/judicial-system-backend/values.staging.yaml @@ -138,6 +138,7 @@ secrets: NOVA_PASSWORD: '/k8s/judicial-system/NOVA_PASSWORD' NOVA_URL: '/k8s/judicial-system/NOVA_URL' NOVA_USERNAME: '/k8s/judicial-system/NOVA_USERNAME' + POLICE_INSTITUTIONS_EMAILS: '/k8s/judicial-system/POLICE_INSTITUTIONS_EMAILS' PRISON_ADMIN_EMAIL: '/k8s/judicial-system/PRISON_ADMIN_EMAIL' PRISON_ADMIN_INDICTMENT_EMAILS: '/k8s/judicial-system/PRISON_ADMIN_INDICTMENT_EMAILS' PRISON_EMAIL: '/k8s/judicial-system/PRISON_EMAIL' diff --git a/libs/judicial-system/message/src/lib/message.ts b/libs/judicial-system/message/src/lib/message.ts index c8650bc09256..c93854bab4e6 100644 --- a/libs/judicial-system/message/src/lib/message.ts +++ b/libs/judicial-system/message/src/lib/message.ts @@ -31,6 +31,8 @@ export enum MessageType { NOTIFICATION_DISPATCH = 'NOTIFICATION_DISPATCH', DEFENDANT_NOTIFICATION = 'DEFENDANT_NOTIFICATION', CIVIL_CLAIMANT_NOTIFICATION = 'CIVIL_CLAIMANT_NOTIFICATION', + INDICTMENT_CASE_NOTIFICATION = 'INDICTMENT_CASE_NOTIFICATION', + EVENT_NOTIFICATION_DISPATCH = 'EVENT_NOTIFICATION_DISPATCH', } export const messageEndpoint: { [key in MessageType]: string } = { @@ -68,6 +70,8 @@ export const messageEndpoint: { [key in MessageType]: string } = { NOTIFICATION_DISPATCH: 'notification/dispatch', DEFENDANT_NOTIFICATION: 'defendantNotification', CIVIL_CLAIMANT_NOTIFICATION: 'civilClaimantNotification', + INDICTMENT_CASE_NOTIFICATION: 'indictmentCaseNotification', + EVENT_NOTIFICATION_DISPATCH: 'eventNotification/dispatch', } export type Message = { diff --git a/libs/judicial-system/types/src/index.ts b/libs/judicial-system/types/src/index.ts index 140f6c9bc92a..82a11272ee06 100644 --- a/libs/judicial-system/types/src/index.ts +++ b/libs/judicial-system/types/src/index.ts @@ -19,6 +19,8 @@ export { NotificationDispatchType, DefendantNotificationType, CivilClaimantNotificationType, + IndictmentCaseNotificationType, + EventNotificationType, notificationTypes, } from './lib/notification' export type { Institution } from './lib/institution' diff --git a/libs/judicial-system/types/src/lib/notification.ts b/libs/judicial-system/types/src/lib/notification.ts index e9b8df060e63..347cef7b5852 100644 --- a/libs/judicial-system/types/src/lib/notification.ts +++ b/libs/judicial-system/types/src/lib/notification.ts @@ -20,6 +20,10 @@ export enum CaseNotificationType { CASE_FILES_UPDATED = 'CASE_FILES_UPDATED', } +export enum IndictmentCaseNotificationType { + INDICTMENT_VERDICT_INFO = 'INDICTMENT_VERDICT_INFO', +} + export enum DefendantNotificationType { DEFENDANT_SELECTED_DEFENDER = 'DEFENDANT_SELECTED_DEFENDER', DEFENDER_ASSIGNED = 'DEFENDER_ASSIGNED', @@ -40,34 +44,40 @@ export enum InstitutionNotificationType { INDICTMENTS_WAITING_FOR_CONFIRMATION = 'INDICTMENTS_WAITING_FOR_CONFIRMATION', } +export enum EventNotificationType { + INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR = 'INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR', +} + export enum NotificationType { - HEADS_UP = CaseNotificationType.HEADS_UP, - READY_FOR_COURT = CaseNotificationType.READY_FOR_COURT, - RECEIVED_BY_COURT = CaseNotificationType.RECEIVED_BY_COURT, - COURT_DATE = CaseNotificationType.COURT_DATE, - RULING = CaseNotificationType.RULING, - MODIFIED = CaseNotificationType.MODIFIED, - REVOKED = CaseNotificationType.REVOKED, ADVOCATE_ASSIGNED = CaseNotificationType.ADVOCATE_ASSIGNED, - DEFENDANTS_NOT_UPDATED_AT_COURT = CaseNotificationType.DEFENDANTS_NOT_UPDATED_AT_COURT, - APPEAL_TO_COURT_OF_APPEALS = CaseNotificationType.APPEAL_TO_COURT_OF_APPEALS, - APPEAL_RECEIVED_BY_COURT = CaseNotificationType.APPEAL_RECEIVED_BY_COURT, - APPEAL_STATEMENT = CaseNotificationType.APPEAL_STATEMENT, + APPEAL_CASE_FILES_UPDATED = CaseNotificationType.APPEAL_CASE_FILES_UPDATED, APPEAL_COMPLETED = CaseNotificationType.APPEAL_COMPLETED, APPEAL_JUDGES_ASSIGNED = CaseNotificationType.APPEAL_JUDGES_ASSIGNED, - APPEAL_CASE_FILES_UPDATED = CaseNotificationType.APPEAL_CASE_FILES_UPDATED, + APPEAL_RECEIVED_BY_COURT = CaseNotificationType.APPEAL_RECEIVED_BY_COURT, + APPEAL_STATEMENT = CaseNotificationType.APPEAL_STATEMENT, + APPEAL_TO_COURT_OF_APPEALS = CaseNotificationType.APPEAL_TO_COURT_OF_APPEALS, APPEAL_WITHDRAWN = CaseNotificationType.APPEAL_WITHDRAWN, - INDICTMENT_DENIED = CaseNotificationType.INDICTMENT_DENIED, - INDICTMENT_RETURNED = CaseNotificationType.INDICTMENT_RETURNED, CASE_FILES_UPDATED = CaseNotificationType.CASE_FILES_UPDATED, + COURT_DATE = CaseNotificationType.COURT_DATE, DEFENDANT_SELECTED_DEFENDER = DefendantNotificationType.DEFENDANT_SELECTED_DEFENDER, + DEFENDANTS_NOT_UPDATED_AT_COURT = CaseNotificationType.DEFENDANTS_NOT_UPDATED_AT_COURT, DEFENDER_ASSIGNED = DefendantNotificationType.DEFENDER_ASSIGNED, + HEADS_UP = CaseNotificationType.HEADS_UP, + INDICTMENT_DENIED = CaseNotificationType.INDICTMENT_DENIED, + INDICTMENT_RETURNED = CaseNotificationType.INDICTMENT_RETURNED, INDICTMENT_SENT_TO_PRISON_ADMIN = DefendantNotificationType.INDICTMENT_SENT_TO_PRISON_ADMIN, + INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR = EventNotificationType.INDICTMENT_SENT_TO_PUBLIC_PROSECUTOR, + INDICTMENT_VERDICT_INFO = IndictmentCaseNotificationType.INDICTMENT_VERDICT_INFO, INDICTMENT_WITHDRAWN_FROM_PRISON_ADMIN = DefendantNotificationType.INDICTMENT_WITHDRAWN_FROM_PRISON_ADMIN, - SERVICE_SUCCESSFUL = SubpoenaNotificationType.SERVICE_SUCCESSFUL, + INDICTMENTS_WAITING_FOR_CONFIRMATION = InstitutionNotificationType.INDICTMENTS_WAITING_FOR_CONFIRMATION, + MODIFIED = CaseNotificationType.MODIFIED, + READY_FOR_COURT = CaseNotificationType.READY_FOR_COURT, + RECEIVED_BY_COURT = CaseNotificationType.RECEIVED_BY_COURT, + REVOKED = CaseNotificationType.REVOKED, + RULING = CaseNotificationType.RULING, SERVICE_FAILED = SubpoenaNotificationType.SERVICE_FAILED, + SERVICE_SUCCESSFUL = SubpoenaNotificationType.SERVICE_SUCCESSFUL, SPOKESPERSON_ASSIGNED = CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, - INDICTMENTS_WAITING_FOR_CONFIRMATION = InstitutionNotificationType.INDICTMENTS_WAITING_FOR_CONFIRMATION, } export const notificationTypes = Object.values(NotificationType)