diff --git a/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts b/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts index 5b65a20a1abe..1087fb20dd1f 100644 --- a/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts +++ b/apps/judicial-system/backend/src/app/modules/case/internalCase.controller.ts @@ -77,6 +77,24 @@ export class InternalCaseController { return this.internalCaseService.archive() } + @Post('cases/postHearingArrangements/:date') + @ApiOkResponse({ + type: Case, + isArray: true, + description: + 'Fetch all cases that have court hearing arrangements for a given date', + }) + async postHearingArrangements(@Param('date') date: Date): Promise { + this.logger.debug( + `Post internal summary of all cases that have court hearing arrangement at ${date}`, + ) + + const cases = await this.internalCaseService.getCaseHearingArrangements( + date, + ) + await this.eventService.postDailyHearingArrangementEvents(date, cases) + } + @Get('cases/indictments/defendant/:defendantNationalId') @ApiOkResponse({ type: Case, 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 d0e54067af7f..6270725b5314 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 @@ -520,6 +520,29 @@ export class InternalCaseService { return { caseArchived: true } } + async getCaseHearingArrangements(date: Date): Promise { + const startOfDay = new Date(date.setHours(0, 0, 0, 0)) + const endOfDay = new Date(date.setHours(23, 59, 59, 999)) + + return this.caseModel.findAll({ + include: [ + { + model: DateLog, + as: 'dateLogs', + where: { + date_type: ['ARRAIGNMENT_DATE', 'COURT_DATE'], + date: { + [Op.gte]: startOfDay, + [Op.lte]: endOfDay, + }, + }, + required: true, + }, + ], + order: [[{ model: DateLog, as: 'dateLogs' }, 'date', 'ASC']], + }) + } + async deliverProsecutorToCourt( theCase: Case, user: TUser, 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 22e6be63d5e0..17cdb20a358e 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 @@ -30,7 +30,6 @@ import type { User as TUser } from '@island.is/judicial-system/types' import { CaseState, CaseType, - DefendantEventType, indictmentCases, investigationCases, restrictionCases, @@ -39,7 +38,6 @@ import { import { nowFactory } from '../../factories' import { defenderRule, prisonSystemStaffRule } from '../../guards' -import { DefendantService } from '../defendant' import { EventService } from '../event' import { User } from '../user' import { TransitionCaseDto } from './dto/transitionCase.dto' diff --git a/apps/judicial-system/backend/src/app/modules/event/event.service.ts b/apps/judicial-system/backend/src/app/modules/event/event.service.ts index 9ab7679e86ae..2b5170a050af 100644 --- a/apps/judicial-system/backend/src/app/modules/event/event.service.ts +++ b/apps/judicial-system/backend/src/app/modules/event/event.service.ts @@ -173,6 +173,51 @@ export class EventService { } } + async postDailyHearingArrangementEvents(date: Date, cases: Case[]) { + const title = `:judge: Fyrirtökur ${formatDate(date)}` + + const arrangementTexts = cases.map((theCase) => { + return `>${theCase.courtCaseNumber}: ${ + formatDate( + DateLog.courtDate(theCase.dateLogs)?.date ?? + DateLog.arraignmentDate(theCase.dateLogs)?.date, + 'p', + ) ?? date + }` + }) + + const arrangementSummary = + arrangementTexts.length > 0 + ? arrangementTexts.join('\n') + : '>Engar fyrirtökur á dagskrá' + + try { + if (!this.config.url) { + return + } + + await fetch(`${this.config.url}`, { + method: 'POST', + headers: { 'Content-type': 'application/json' }, + body: JSON.stringify({ + blocks: [ + { + type: 'section', + text: { + type: 'mrkdwn', + text: `*${title}*\n${arrangementSummary}`, + }, + }, + ], + }), + }) + } catch (error) { + this.logger.error(`Failed to post court hearing arrangement summary`, { + error, + }) + } + } + async postErrorEvent( message: string, info: { [key: string]: string | boolean | Date | undefined }, diff --git a/apps/judicial-system/scheduler/src/app/app.service.ts b/apps/judicial-system/scheduler/src/app/app.service.ts index 015d825af410..e906154c3968 100644 --- a/apps/judicial-system/scheduler/src/app/app.service.ts +++ b/apps/judicial-system/scheduler/src/app/app.service.ts @@ -1,6 +1,6 @@ import fetch from 'node-fetch' -import { Inject, Injectable } from '@nestjs/common' +import { BadGatewayException, Inject, Injectable } from '@nestjs/common' import { type Logger, LOGGER_PROVIDER } from '@island.is/logging' import { type ConfigType } from '@island.is/nest/config' @@ -24,12 +24,8 @@ export class AppService { @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} - async run() { - this.logger.info('Scheduler starting') - - const startTime = now() - - this.messageService + private addMessagesForIndictmentsWaitingForConfirmationToQueue() { + return this.messageService .sendMessagesToQueue([ { type: MessageType.NOTIFICATION_DISPATCH, @@ -42,6 +38,10 @@ export class AppService { // Tolerate failure, but log this.logger.error('Failed to dispatch notifications', { reason }), ) + } + + private async archiveCases() { + const startTime = now() let done = false @@ -76,6 +76,38 @@ export class AppService { !done && minutesBetween(startTime, now()) < this.config.timeToLiveMinutes ) + } + + private async postDailyHearingArrangementSummary() { + const today = now() + try { + const res = await fetch( + `${this.config.backendUrl}/api/internal/cases/postHearingArrangements/${today}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + authorization: `Bearer ${this.config.backendAccessToken}`, + }, + }, + ) + + if (!res.ok) { + throw new BadGatewayException( + 'Unexpected error occurred while fetching cases', + ) + } + } catch (error) { + throw new BadGatewayException(`Failed to fetch cases: ${error.message}`) + } + } + + async run() { + this.logger.info('Scheduler starting') + + await this.addMessagesForIndictmentsWaitingForConfirmationToQueue() + await this.archiveCases() + await this.postDailyHearingArrangementSummary() this.logger.info('Scheduler done') } diff --git a/apps/judicial-system/scheduler/src/app/test/run.spec.ts b/apps/judicial-system/scheduler/src/app/test/run.spec.ts index df149409eeab..81a6d55d3e13 100644 --- a/apps/judicial-system/scheduler/src/app/test/run.spec.ts +++ b/apps/judicial-system/scheduler/src/app/test/run.spec.ts @@ -28,6 +28,7 @@ describe('AppService - Run', () => { beforeEach(() => { mockNow.mockClear() mockFetch.mockClear() + mockNow.mockReturnValue(new Date('2020-01-01T00:01:00.000Z')) givenWhenThen = async (): Promise => { @@ -75,7 +76,7 @@ describe('AppService - Run', () => { body: { type: 'INDICTMENTS_WAITING_FOR_CONFIRMATION' }, }, ]) - expect(fetch).toHaveBeenCalledTimes(3) + expect(fetch).toHaveBeenCalledTimes(4) expect(fetch).toHaveBeenCalledWith( `${appModuleConfig().backendUrl}/api/internal/cases/archive`, { @@ -86,6 +87,20 @@ describe('AppService - Run', () => { }, }, ) + expect(fetch).toHaveBeenCalledWith( + `${ + appModuleConfig().backendUrl + }/api/internal/cases/postHearingArrangements/${new Date( + '2020-01-01T00:01:00.000Z', + )}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + authorization: `Bearer ${appModuleConfig().backendAccessToken}`, + }, + }, + ) }) }) @@ -102,8 +117,17 @@ describe('AppService - Run', () => { await givenWhenThen() }) - it('should call the backend twice', () => { - expect(fetch).toHaveBeenCalledTimes(2) + it('should attempt archiving twice', () => { + expect(fetch).toHaveBeenNthCalledWith( + 1, + `${appModuleConfig().backendUrl}/api/internal/cases/archive`, + expect.any(Object), + ) + expect(fetch).toHaveBeenNthCalledWith( + 2, + `${appModuleConfig().backendUrl}/api/internal/cases/archive`, + expect.any(Object), + ) }) })