diff --git a/apps/judicial-system/backend/src/app/formatters/formatters.ts b/apps/judicial-system/backend/src/app/formatters/formatters.ts index d48904e27885..b5859c04b70b 100644 --- a/apps/judicial-system/backend/src/app/formatters/formatters.ts +++ b/apps/judicial-system/backend/src/app/formatters/formatters.ts @@ -668,49 +668,6 @@ export const formatCustodyRestrictions = ( }) } -export const formatAdvocateAssignedEmailNotification = ( - formatMessage: FormatMessage, - theCase: Case, - advocateType: AdvocateType, - overviewUrl?: string, -): SubjectAndBody => { - const subject = - advocateType === AdvocateType.DEFENDER - ? formatMessage( - notifications.advocateAssignedEmail.subjectAccessToCaseFiles, - { - court: capitalize(theCase.court?.name ?? ''), - }, - ) - : formatMessage(notifications.advocateAssignedEmail.subjectAccess, { - courtCaseNumber: theCase.courtCaseNumber, - }) - - const body = - advocateType === AdvocateType.DEFENDER - ? formatMessage( - notifications.advocateAssignedEmail.bodyAccessToCaseFiles, - { - defenderHasAccessToRVG: Boolean(overviewUrl), - courtCaseNumber: capitalize(theCase.courtCaseNumber ?? ''), - court: theCase.court?.name ?? '', - courtName: theCase.court?.name.replace('dómur', 'dómi') ?? '', - linkStart: ``, - linkEnd: '', - }, - ) - : formatMessage(notifications.advocateAssignedEmail.bodyAccess, { - defenderHasAccessToRVG: Boolean(overviewUrl), - court: theCase.court?.name, - advocateType, - courtCaseNumber: capitalize(theCase.courtCaseNumber ?? ''), - linkStart: ``, - linkEnd: '', - }) - - return { body, subject } -} - export const formatCourtOfAppealJudgeAssignedEmailNotification = ( formatMessage: FormatMessage, caseNumber: string, diff --git a/apps/judicial-system/backend/src/app/formatters/index.ts b/apps/judicial-system/backend/src/app/formatters/index.ts index 48f8af67b53e..063734cdaacb 100644 --- a/apps/judicial-system/backend/src/app/formatters/index.ts +++ b/apps/judicial-system/backend/src/app/formatters/index.ts @@ -21,7 +21,6 @@ export { formatProsecutorReceivedByCourtSmsNotification, formatDefenderCourtDateLinkEmailNotification, formatDefenderResubmittedToCourtEmailNotification, - formatAdvocateAssignedEmailNotification, formatCourtIndictmentReadyForCourtEmailNotification, formatDefenderRoute, formatDefenderReadyForCourtEmailNotification, diff --git a/apps/judicial-system/backend/src/app/messages/notifications.ts b/apps/judicial-system/backend/src/app/messages/notifications.ts index fe12aea5011d..258eeedc852f 100644 --- a/apps/judicial-system/backend/src/app/messages/notifications.ts +++ b/apps/judicial-system/backend/src/app/messages/notifications.ts @@ -607,32 +607,6 @@ export const notifications = { 'Notaður sem texti í tölvupósti til verjanda vegna breytingar á lengd gæslu/einangrunar/vistunar þar sem úrskurðað var í einangrun.', }, }), - advocateAssignedEmail: defineMessages({ - subjectAccessToCaseFiles: { - id: 'judicial.system.backend:notifications.defender_assigned_email.subject_access_to_case_files', - defaultMessage: '{court} - aðgangur að málsgögnum', - description: - 'Fyrirsögn í pósti til verjanda þegar hann er skráður á mál.', - }, - subjectAccess: { - id: 'judicial.system.backend:notifications.defender_assigned_email.subject_access', - defaultMessage: 'Skráning í máli {courtCaseNumber}', - description: - 'Fyrirsögn í pósti til verjanda þegar hann er skráður á mál.', - }, - bodyAccessToCaseFiles: { - id: 'judicial.system.backend:notifications.defender_assigned_email.body_access_to_case_files', - defaultMessage: - '{court} hefur skráð þig verjanda í máli {courtCaseNumber}.

{defenderHasAccessToRVG, select, true {Gögn málsins eru aðgengileg á {linkStart}yfirlitssíðu málsins í Réttarvörslugátt{linkEnd}} other {Þú getur nálgast gögn málsins hjá {courtName} ef þau hafa ekki þegar verið afhent}}.', - description: 'Texti í pósti til verjanda þegar hann er skráður á mál.', - }, - bodyAccess: { - id: 'judicial.system.backend:notifications.defender_assigned_email.body_access', - defaultMessage: - '{court} hefur skráð þig {advocateType, select, LAWYER {lögmann einkaréttarkröfuhafa} LEGAL_RIGHTS_PROTECTOR {réttargæslumann einkaréttarkröfuhafa} other {verjanda}} í máli {courtCaseNumber}.

{defenderHasAccessToRVG, select, true {Sjá nánar á {linkStart}yfirlitssíðu málsins í Réttarvörslugátt{linkEnd}} other {Þú getur nálgast málið hjá {courtName}.}}.', - description: 'Texti í pósti til verjanda þegar hann er skráður á mál.', - }, - }), defendantsNotUpdatedAtCourt: defineMessages({ subject: { id: 'judicial.system.backend:notifications.defendants_not_updated_at_court.subject', diff --git a/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts b/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts index 950e62bf84da..285092718ba5 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/civilClaimant.service.ts @@ -7,7 +7,11 @@ import type { Logger } from '@island.is/logging' import { LOGGER_PROVIDER } from '@island.is/logging' import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' -import { CaseState } from '@island.is/judicial-system/types' +import { MessageService, MessageType } from '@island.is/judicial-system/message' +import { + CaseState, + CivilClaimantNotificationType, +} from '@island.is/judicial-system/types' import { Case } from '../case/models/case.model' import { UpdateCivilClaimantDto } from './dto/updateCivilClaimant.dto' @@ -18,6 +22,7 @@ export class CivilClaimantService { constructor( @InjectModel(CivilClaimant) private readonly civilClaimantModel: typeof CivilClaimant, + private readonly messageService: MessageService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -27,6 +32,24 @@ export class CivilClaimantService { }) } + private async sendUpdateCivilClaimantMessages( + update: UpdateCivilClaimantDto, + updatedCivilClaimant: CivilClaimant, + ): Promise { + if (update.isSpokespersonConfirmed === true) { + return this.messageService.sendMessagesToQueue([ + { + type: MessageType.CIVIL_CLAIMANT_NOTIFICATION, + caseId: updatedCivilClaimant.caseId, + body: { + type: CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + }, + elementId: updatedCivilClaimant.id, + }, + ]) + } + } + async update( caseId: string, civilClaimantId: string, @@ -49,6 +72,8 @@ export class CivilClaimantService { throw new Error(`Could not update civil claimant ${civilClaimantId}`) } + await this.sendUpdateCivilClaimantMessages(update, civilClaimants[0]) + return civilClaimants[0] } diff --git a/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimaint.decorator.ts b/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimaint.decorator.ts new file mode 100644 index 000000000000..9f508c5e60e5 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimaint.decorator.ts @@ -0,0 +1,7 @@ +import { createParamDecorator } from '@nestjs/common' + +import { CivilClaimant } from '../models/civilClaimant.model' + +export const CurrentCivilClaimant = createParamDecorator( + (data, { args: [_1, { req }] }): CivilClaimant => req.civilClaimant, +) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimantExists.guard.ts b/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimantExists.guard.ts new file mode 100644 index 000000000000..401886582d41 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/guards/civilClaimantExists.guard.ts @@ -0,0 +1,42 @@ +import { + BadRequestException, + CanActivate, + ExecutionContext, + Injectable, + NotFoundException, +} from '@nestjs/common' + +import { Case } from '../../case' + +@Injectable() +export class CivilClaimantExistsGuard implements CanActivate { + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest() + + const theCase: Case = request.case + + if (!theCase) { + throw new BadRequestException('Missing case') + } + + const civilClaimantId = request.params.civilClaimantId + + if (!civilClaimantId) { + throw new BadRequestException('Missing civil claimant id') + } + + const civilClaimant = theCase.civilClaimants?.find( + (civilClaimants) => civilClaimants.id === civilClaimantId, + ) + + if (!civilClaimant) { + throw new NotFoundException( + `Civil claimant ${civilClaimantId} of case ${theCase.id} does not exist`, + ) + } + + request.civilClaimant = civilClaimant + + return true + } +} diff --git a/apps/judicial-system/backend/src/app/modules/defendant/guards/test/civilClaimantExistsGuard.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/guards/test/civilClaimantExistsGuard.spec.ts new file mode 100644 index 000000000000..4e5cd39db640 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/guards/test/civilClaimantExistsGuard.spec.ts @@ -0,0 +1,117 @@ +import { uuid } from 'uuidv4' + +import { + BadRequestException, + ExecutionContext, + NotFoundException, +} from '@nestjs/common' + +import { CivilClaimantExistsGuard } from '../civilClaimantExists.guard' + +interface Then { + result: boolean + error: Error +} + +type GivenWhenThen = () => Promise + +describe('Civil Claimant Exists Guard', () => { + const mockRequest = jest.fn() + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + givenWhenThen = async (): Promise => { + const guard = new CivilClaimantExistsGuard() + const then = {} as Then + + try { + then.result = guard.canActivate({ + switchToHttp: () => ({ getRequest: mockRequest }), + } as unknown as ExecutionContext) + } catch (error) { + then.error = error as Error + } + + return then + } + }) + + describe('civil claimant exists', () => { + const caseId = uuid() + const civilClaimantId = uuid() + const civilClaimant = { id: civilClaimantId, caseId } + const theCase = { id: caseId, civilClaimants: [civilClaimant] } + const request = { + params: { caseId, civilClaimantId }, + case: theCase, + civilClaimant: undefined, + } + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce(request) + + then = await givenWhenThen() + }) + + it('should activate', () => { + expect(then.result).toBe(true) + expect(request.civilClaimant).toBe(civilClaimant) + }) + }) + + describe('civil claimant does not exist', () => { + const caseId = uuid() + const civilClaimantId = uuid() + const theCase = { id: caseId, civilClaimants: [] } + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce({ + params: { caseId, civilClaimantId }, + case: theCase, + }) + + then = await givenWhenThen() + }) + + it('should throw NotFoundException', () => { + expect(then.error).toBeInstanceOf(NotFoundException) + expect(then.error.message).toBe( + `Civil claimant ${civilClaimantId} of case ${caseId} does not exist`, + ) + }) + }) + + describe('missing case', () => { + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce({ params: {} }) + + then = await givenWhenThen() + }) + + it('should throw BadRequestException', () => { + expect(then.error).toBeInstanceOf(BadRequestException) + expect(then.error.message).toBe('Missing case') + }) + }) + + describe('missing civil claimant id', () => { + const caseId = uuid() + const theCase = { id: caseId, civilClaimants: [] } + let then: Then + + beforeEach(async () => { + mockRequest.mockReturnValueOnce({ params: { caseId }, case: theCase }) + + then = await givenWhenThen() + }) + + it('should throw BadRequestException', () => { + expect(then.error).toBeInstanceOf(BadRequestException) + expect(then.error.message).toBe('Missing civil claimant id') + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/index.ts b/apps/judicial-system/backend/src/app/modules/defendant/index.ts index d9914e3dcda4..5fb8a9c29896 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/index.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/index.ts @@ -1,6 +1,9 @@ export { Defendant } from './models/defendant.model' export { DefendantService } from './defendant.service' -export { CivilClaimant } from './models/civilClaimant.model' export { DefendantExistsGuard } from './guards/defendantExists.guard' export { CurrentDefendant } from './guards/defendant.decorator' + +export { CivilClaimant } from './models/civilClaimant.model' export { CivilClaimantService } from './civilClaimant.service' +export { CivilClaimantExistsGuard } from './guards/civilClaimantExists.guard' +export { CurrentCivilClaimant } from './guards/civilClaimaint.decorator' diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/create.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/create.spec.ts new file mode 100644 index 000000000000..4684346f3209 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/create.spec.ts @@ -0,0 +1,80 @@ +import { uuid } from 'uuidv4' + +import { createTestingDefendantModule } from '../createTestingDefendantModule' + +import { Case } from '../../../case' +import { CivilClaimant } from '../../models/civilClaimant.model' + +interface Then { + result: CivilClaimant + error: Error +} + +type GivenWhenThen = (caseId?: string) => Promise + +describe('CivilClaimantController - Create', () => { + const caseId = uuid() + const civilClaimantId = uuid() + const theCase = { id: caseId } as Case + const civilClaimantToCreate = { + caseId, + } + const createdCivilClaimant = { id: civilClaimantId, caseId } + + let mockCivilClaimantModel: typeof CivilClaimant + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + const { civilClaimantModel, civilClaimantController } = + await createTestingDefendantModule() + + mockCivilClaimantModel = civilClaimantModel + + const mockCreate = mockCivilClaimantModel.create as jest.Mock + mockCreate.mockResolvedValue(createdCivilClaimant) + + givenWhenThen = async () => { + const then = {} as Then + + await civilClaimantController + .create(theCase.id, theCase) + .then((result) => (then.result = result)) + .catch((error) => (then.error = error)) + + return then + } + }) + + describe('civil claimant creation', () => { + let then: Then + + beforeEach(async () => { + then = await givenWhenThen(caseId) + }) + it('should create a civil claimant', () => { + expect(mockCivilClaimantModel.create).toHaveBeenCalledWith( + civilClaimantToCreate, + ) + }) + + it('should return the created civil claimant', () => { + expect(then.result).toEqual(createdCivilClaimant) + }) + }) + + describe('civil claimant creation fails', () => { + let then: Then + + beforeEach(async () => { + const mockCreate = mockCivilClaimantModel.create as jest.Mock + mockCreate.mockRejectedValue(new Error('Test error')) + + then = await givenWhenThen(caseId) + }) + + it('should throw an error', () => { + expect(then.error).toBeInstanceOf(Error) + expect(then.error.message).toEqual('Test error') + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createGuards.spec.ts new file mode 100644 index 000000000000..738952366261 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createGuards.spec.ts @@ -0,0 +1,25 @@ +import { CanActivate } from '@nestjs/common' + +import { CaseExistsGuard, CaseWriteGuard } from '../../../case' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Create guards', () => { + let guards: Array CanActivate> + const expectedGuards = [CaseExistsGuard, CaseWriteGuard] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + CivilClaimantController.prototype.create, + ) + }) + + it('should have the correct guards in the correct order', () => { + expect(guards).toHaveLength(expectedGuards.length) + + expectedGuards.forEach((expectedGuard, index) => { + const guardInstance = new guards[index]() + expect(guardInstance).toBeInstanceOf(expectedGuard) + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createRolesRules.spec.ts new file mode 100644 index 000000000000..326946023e4f --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/createRolesRules.spec.ts @@ -0,0 +1,35 @@ +import { + districtCourtAssistantRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + prosecutorRepresentativeRule, + prosecutorRule, +} from '../../../../guards' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Create rules', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let rules: any[] + + const expectedRules = [ + prosecutorRule, + prosecutorRepresentativeRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + districtCourtAssistantRule, + ] + + beforeEach(() => { + rules = Reflect.getMetadata( + 'roles-rules', + CivilClaimantController.prototype.create, + ) + }) + + it('should give permission to roles', () => { + expect(rules).toHaveLength(expectedRules.length) + expectedRules.forEach((expectedRule) => + expect(rules).toContain(expectedRule), + ) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/delete.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/delete.spec.ts new file mode 100644 index 000000000000..8a2c17bc5e10 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/delete.spec.ts @@ -0,0 +1,79 @@ +import { uuid } from 'uuidv4' + +import { createTestingDefendantModule } from '../createTestingDefendantModule' + +import { CivilClaimant } from '../../models/civilClaimant.model' +import { DeleteCivilClaimantResponse } from '../../models/deleteCivilClaimant.response' + +interface Then { + result: DeleteCivilClaimantResponse + error: Error +} + +type GivenWhenThen = ( + caseId?: string, + civilClaimaintId?: string, +) => Promise + +describe('CivilClaimantController - Delete', () => { + const caseId = uuid() + const civilClaimantId = uuid() + + let mockCivilClaimantModel: typeof CivilClaimant + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + const { civilClaimantController, civilClaimantModel } = + await createTestingDefendantModule() + + mockCivilClaimantModel = civilClaimantModel + + const mockDestroy = mockCivilClaimantModel.destroy as jest.Mock + mockDestroy.mockRejectedValue(new Error('Test error')) + + givenWhenThen = async () => { + const then = {} as Then + + try { + then.result = await civilClaimantController.delete( + caseId, + civilClaimantId, + ) + } catch (error) { + then.error = error as Error + } + + return then + } + }) + + describe('civil claimant deleted', () => { + let then: Then + + beforeEach(async () => { + const mockDestroy = mockCivilClaimantModel.destroy as jest.Mock + mockDestroy.mockResolvedValue(1) + + then = await givenWhenThen(caseId, civilClaimantId) + }) + it('should delete civil claimant', () => { + expect(mockCivilClaimantModel.destroy).toHaveBeenCalledWith({ + where: { caseId, id: civilClaimantId }, + }) + expect(then.result).toEqual({ deleted: true }) + }) + }) + + describe('civil claimant deletion fails', () => { + let then: Then + + beforeEach(async () => { + then = await givenWhenThen() + }) + + it('should throw Error', () => { + expect(then.error).toBeInstanceOf(Error) + expect(then.error.message).toBe('Test error') + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteGuards.spec.ts new file mode 100644 index 000000000000..20e8ef89e2f6 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteGuards.spec.ts @@ -0,0 +1,25 @@ +import { CanActivate } from '@nestjs/common' + +import { CaseExistsGuard, CaseWriteGuard } from '../../../case' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Delete guards', () => { + let guards: Array CanActivate> + const expectedGuards = [CaseExistsGuard, CaseWriteGuard] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + CivilClaimantController.prototype.delete, + ) + }) + + it('should have the correct guards in the correct order', () => { + expect(guards).toHaveLength(expectedGuards.length) + + expectedGuards.forEach((expectedGuard, index) => { + const guardInstance = new guards[index]() + expect(guardInstance).toBeInstanceOf(expectedGuard) + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteRolesRules.spec.ts new file mode 100644 index 000000000000..46039a55bc8b --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/deleteRolesRules.spec.ts @@ -0,0 +1,26 @@ +import { + prosecutorRepresentativeRule, + prosecutorRule, +} from '../../../../guards' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Delete rules', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let rules: any[] + + const expectedRules = [prosecutorRule, prosecutorRepresentativeRule] + + beforeEach(() => { + rules = Reflect.getMetadata( + 'roles-rules', + CivilClaimantController.prototype.delete, + ) + }) + + it('should give permission to roles', () => { + expect(rules).toHaveLength(expectedRules.length) + expectedRules.forEach((expectedRule) => + expect(rules).toContain(expectedRule), + ) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/update.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/update.spec.ts new file mode 100644 index 000000000000..d8661906faa4 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/update.spec.ts @@ -0,0 +1,140 @@ +import { uuid } from 'uuidv4' + +import { MessageService, MessageType } from '@island.is/judicial-system/message' +import { CivilClaimantNotificationType } from '@island.is/judicial-system/types' + +import { createTestingDefendantModule } from '../createTestingDefendantModule' + +import { UpdateCivilClaimantDto } from '../../dto/updateCivilClaimant.dto' +import { CivilClaimant } from '../../models/civilClaimant.model' + +interface Then { + result: CivilClaimant + error: Error +} + +type GivenWhenThen = ( + caseId: string, + civilClaimantId: string, + updateData: UpdateCivilClaimantDto, +) => Promise + +describe('CivilClaimantController - Update', () => { + const caseId = uuid() + const civilClaimantId = uuid() + const civilClaimaint = { + id: civilClaimantId, + caseId, + nationalId: uuid(), + name: 'Original Name', + } as CivilClaimant + + let mockMessageService: MessageService + let mockCivilClaimantModel: typeof CivilClaimant + let givenWhenThen: GivenWhenThen + + beforeEach(async () => { + const { messageService, civilClaimantModel, civilClaimantController } = + await createTestingDefendantModule() + + mockMessageService = messageService + mockCivilClaimantModel = civilClaimantModel + + givenWhenThen = async ( + caseId: string, + civilClaimantId: string, + updateData: UpdateCivilClaimantDto, + ) => { + const then = {} as Then + + await civilClaimantController + .update(caseId, civilClaimantId, updateData) + .then((result) => (then.result = result)) + .catch((error) => (then.error = error)) + + return then + } + }) + + describe('civil claimant updated', () => { + const civilClaimantUpdate = { name: 'Updated Name' } + const updatedCivilClaimant = { + id: civilClaimantId, + caseId, + ...civilClaimantUpdate, + } + let then: Then + + beforeEach(async () => { + const mockUpdate = mockCivilClaimantModel.update as jest.Mock + mockUpdate.mockResolvedValueOnce([1, [updatedCivilClaimant]]) + + then = await givenWhenThen(caseId, civilClaimantId, civilClaimantUpdate) + }) + + it('should update the civil claimant', () => { + expect(mockCivilClaimantModel.update).toHaveBeenCalledWith( + civilClaimantUpdate, + { + where: { id: civilClaimantId, caseId }, + returning: true, + }, + ) + expect(mockMessageService.sendMessagesToQueue).not.toHaveBeenCalled() + }) + + it('should return the updated civil claimant', () => { + expect(then.result).toBe(updatedCivilClaimant) + }) + }) + + describe('civil claimant spokesperson confirmed', () => { + const civilClaimantUpdate = { isSpokespersonConfirmed: true } + const updatedCivilClaimant = { + id: civilClaimantId, + caseId, + ...civilClaimantUpdate, + } + let then: Then + + beforeEach(async () => { + const mockUpdate = mockCivilClaimantModel.update as jest.Mock + mockUpdate.mockResolvedValueOnce([1, [updatedCivilClaimant]]) + + then = await givenWhenThen(caseId, civilClaimantId, civilClaimantUpdate) + }) + + it('should queue spokesperson assigned message', () => { + expect(mockMessageService.sendMessagesToQueue).toHaveBeenCalledWith([ + { + type: MessageType.CIVIL_CLAIMANT_NOTIFICATION, + caseId, + elementId: civilClaimantId, + body: { + type: CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + }, + }, + ]) + }) + + it('should return the updated civil claimant', () => { + expect(then.result).toBe(updatedCivilClaimant) + }) + }) + + describe('civil claimant update fails', () => { + let then: Then + + beforeEach(async () => { + const mockUpdate = mockCivilClaimantModel.update as jest.Mock + mockUpdate.mockRejectedValue(new Error('Test error')) + + then = await givenWhenThen(caseId, civilClaimantId, {}) + }) + + it('should throw an error', () => { + expect(then.error).toBeInstanceOf(Error) + expect(then.error.message).toEqual('Test error') + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateGuards.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateGuards.spec.ts new file mode 100644 index 000000000000..d333af01f86f --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateGuards.spec.ts @@ -0,0 +1,25 @@ +import { CanActivate } from '@nestjs/common' + +import { CaseExistsGuard, CaseWriteGuard } from '../../../case' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Update guards', () => { + let guards: Array CanActivate> + const expectedGuards = [CaseExistsGuard, CaseWriteGuard] + + beforeEach(() => { + guards = Reflect.getMetadata( + '__guards__', + CivilClaimantController.prototype.update, + ) + }) + + it('should have the correct guards in the correct order', () => { + expect(guards).toHaveLength(expectedGuards.length) + + expectedGuards.forEach((expectedGuard, index) => { + const guardInstance = new guards[index]() + expect(guardInstance).toBeInstanceOf(expectedGuard) + }) + }) +}) diff --git a/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateRolesRules.spec.ts b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateRolesRules.spec.ts new file mode 100644 index 000000000000..38a579850480 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/defendant/test/civilClaimantController/updateRolesRules.spec.ts @@ -0,0 +1,35 @@ +import { + districtCourtAssistantRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + prosecutorRepresentativeRule, + prosecutorRule, +} from '../../../../guards' +import { CivilClaimantController } from '../../civilClaimant.controller' + +describe('CivilClaimantController - Update rules', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let rules: any[] + + const expectedRules = [ + prosecutorRule, + prosecutorRepresentativeRule, + districtCourtJudgeRule, + districtCourtRegistrarRule, + districtCourtAssistantRule, + ] + + beforeEach(() => { + rules = Reflect.getMetadata( + 'roles-rules', + CivilClaimantController.prototype.update, + ) + }) + + it('should give permission to roles', () => { + expect(rules).toHaveLength(expectedRules.length) + expectedRules.forEach((expectedRule) => + expect(rules).toContain(expectedRule), + ) + }) +}) 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 3b18ac9b8c8c..7ca4abf0ae2d 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 @@ -13,9 +13,12 @@ import { MessageService } from '@island.is/judicial-system/message' import { CaseService } from '../../case' import { CourtService } from '../../court' import { UserService } from '../../user' +import { CivilClaimantController } from '../civilClaimant.controller' +import { CivilClaimantService } from '../civilClaimant.service' import { DefendantController } from '../defendant.controller' import { DefendantService } from '../defendant.service' import { InternalDefendantController } from '../internalDefendant.controller' +import { CivilClaimant } from '../models/civilClaimant.model' import { Defendant } from '../models/defendant.model' jest.mock('@island.is/judicial-system/message') @@ -26,7 +29,11 @@ jest.mock('../../case/case.service') export const createTestingDefendantModule = async () => { const defendantModule = await Test.createTestingModule({ imports: [ConfigModule.forRoot({ load: [sharedAuthModuleConfig] })], - controllers: [DefendantController, InternalDefendantController], + controllers: [ + DefendantController, + InternalDefendantController, + CivilClaimantController, + ], providers: [ SharedAuthModule, MessageService, @@ -52,7 +59,19 @@ export const createTestingDefendantModule = async () => { findByPk: jest.fn(), }, }, + { + provide: getModelToken(CivilClaimant), + useValue: { + findOne: jest.fn(), + findAll: jest.fn(), + create: jest.fn(), + update: jest.fn(), + destroy: jest.fn(), + findByPk: jest.fn(), + }, + }, DefendantService, + CivilClaimantService, ], }).compile() @@ -77,6 +96,17 @@ export const createTestingDefendantModule = async () => { InternalDefendantController, ) + const civilClaimantModel = await defendantModule.resolve< + typeof CivilClaimant + >(getModelToken(CivilClaimant)) + + const civilClaimantService = + defendantModule.get(CivilClaimantService) + + const civilClaimantController = defendantModule.get( + CivilClaimantController, + ) + defendantModule.close() return { @@ -87,5 +117,8 @@ export const createTestingDefendantModule = async () => { defendantService, defendantController, internalDefendantController, + civilClaimantService, + civilClaimantController, + civilClaimantModel, } } diff --git a/apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts index beca473bbbb5..5f47cbac54c6 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/caseNotification.service.ts @@ -50,7 +50,6 @@ import { } from '@island.is/judicial-system/types' import { - formatAdvocateAssignedEmailNotification, formatCourtHeadsUpSmsNotification, formatCourtIndictmentReadyForCourtEmailNotification, formatCourtOfAppealJudgeAssignedEmailNotification, @@ -1506,17 +1505,7 @@ export class CaseNotificationService extends BaseNotificationService { if (!advocateEmail) { return false } - if (isIndictmentCase(theCase.type)) { - const hasSentNotificationBefore = this.hasReceivedNotification( - CaseNotificationType.ADVOCATE_ASSIGNED, - advocateEmail, - theCase.notifications, - ) - - if (hasSentNotificationBefore) { - return false - } - } else if (isInvestigationCase(theCase.type)) { + if (isInvestigationCase(theCase.type)) { const isDefenderIncludedInSessionArrangements = theCase.sessionArrangements && [ @@ -1527,7 +1516,7 @@ export class CaseNotificationService extends BaseNotificationService { if (!isDefenderIncludedInSessionArrangements) { return false } - } else { + } else if (isRequestCase(theCase.type)) { const hasDefenderBeenNotified = this.hasReceivedNotification( [ CaseNotificationType.READY_FOR_COURT, @@ -1546,70 +1535,12 @@ export class CaseNotificationService extends BaseNotificationService { return true } - private sendAdvocateAssignedNotification( - theCase: Case, - advocateType: AdvocateType, - advocateNationalId?: string, - advocateName?: string, - advocateEmail?: string, - ): Promise { - const { subject, body } = formatAdvocateAssignedEmailNotification( - this.formatMessage, - theCase, - advocateType, - advocateNationalId && - formatDefenderRoute(this.config.clientUrl, theCase.type, theCase.id), - ) - - return this.sendEmail( - subject, - body, - advocateName, - advocateEmail, - undefined, - Boolean(advocateNationalId) === false, - ) - } - private async sendAdvocateAssignedNotifications( theCase: Case, ): Promise { const promises: Promise[] = [] - if (isIndictmentCase(theCase.type)) { - if (theCase.civilClaimants) { - for (const civilClaimant of theCase.civilClaimants) { - const { - spokespersonEmail, - spokespersonIsLawyer, - spokespersonName, - spokespersonNationalId, - hasSpokesperson, - } = civilClaimant - - const shouldSend = - hasSpokesperson && - this.shouldSendAdvocateAssignedNotification( - theCase, - spokespersonEmail, - ) - - if (shouldSend === true) { - promises.push( - this.sendAdvocateAssignedNotification( - theCase, - spokespersonIsLawyer - ? AdvocateType.LAWYER - : AdvocateType.LEGAL_RIGHTS_PROTECTOR, - spokespersonNationalId, - spokespersonName, - spokespersonEmail, - ), - ) - } - } - } - } else if (DateLog.arraignmentDate(theCase.dateLogs)?.date) { + if (DateLog.arraignmentDate(theCase.dateLogs)?.date) { const shouldSend = this.shouldSendAdvocateAssignedNotification( theCase, theCase.defenderEmail, diff --git a/apps/judicial-system/backend/src/app/modules/notification/civilClaimantNotification.service.ts b/apps/judicial-system/backend/src/app/modules/notification/civilClaimantNotification.service.ts new file mode 100644 index 000000000000..c80250801348 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/civilClaimantNotification.service.ts @@ -0,0 +1,171 @@ +import { MessageDescriptor } from '@formatjs/intl' + +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 { DEFENDER_INDICTMENT_ROUTE } from '@island.is/judicial-system/consts' +import { capitalize } from '@island.is/judicial-system/formatters' +import { CivilClaimantNotificationType } from '@island.is/judicial-system/types' + +import { Case } from '../case' +import { CivilClaimant } from '../defendant' +import { EventService } from '../event' +import { DeliverResponse } from './models/deliver.response' +import { Notification, Recipient } from './models/notification.model' +import { BaseNotificationService } from './baseNotification.service' +import { strings } from './civilClaimantNotification.strings' +import { notificationModuleConfig } from './notification.config' + +@Injectable() +export class CivilClaimantNotificationService 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( + civilClaimant: CivilClaimant, + theCase: Case, + notificationType: CivilClaimantNotificationType, + subject: MessageDescriptor, + body: MessageDescriptor, + ) { + const courtName = capitalize(theCase.court?.name) + const courtCaseNumber = theCase.courtCaseNumber + const spokespersonHasAccessToRVG = !!civilClaimant.spokespersonNationalId + + const formattedSubject = this.formatMessage(subject, { + courtName, + courtCaseNumber, + }) + + const formattedBody = this.formatMessage(body, { + courtName, + courtCaseNumber, + spokespersonHasAccessToRVG, + spokespersonIsLawyer: civilClaimant.spokespersonIsLawyer, + linkStart: ``, + linkEnd: '', + }) + const promises: Promise[] = [] + + if (civilClaimant.isSpokespersonConfirmed) { + promises.push( + this.sendEmail( + formattedSubject, + formattedBody, + civilClaimant.spokespersonName, + civilClaimant.spokespersonEmail, + undefined, + true, + ), + ) + } + + const recipients = await Promise.all(promises) + + return this.recordNotification(theCase.id, notificationType, recipients) + } + + private shouldSendSpokespersonAssignedNotification( + theCase: Case, + civilClaimant: CivilClaimant, + ): boolean { + if ( + !civilClaimant.spokespersonEmail || + !civilClaimant.isSpokespersonConfirmed + ) { + return false + } + + const hasSentNotificationBefore = this.hasReceivedNotification( + CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + civilClaimant.spokespersonEmail, + theCase.notifications, + ) + + if (!hasSentNotificationBefore) { + return true + } + + return false + } + + private async sendSpokespersonAssignedNotification( + civilClaimant: CivilClaimant, + theCase: Case, + ): Promise { + const shouldSend = this.shouldSendSpokespersonAssignedNotification( + theCase, + civilClaimant, + ) + + if (shouldSend) { + return this.sendEmails( + civilClaimant, + theCase, + CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + strings.civilClaimantSpokespersonAssignedSubject, + strings.civilClaimantSpokespersonAssignedBody, + ) + } + + // Nothing should be sent so we return a successful response + return { delivered: true } + } + + private sendNotification( + notificationType: CivilClaimantNotificationType, + civilClaimant: CivilClaimant, + theCase: Case, + ): Promise { + switch (notificationType) { + case CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED: + return this.sendSpokespersonAssignedNotification(civilClaimant, theCase) + default: + throw new InternalServerErrorException( + `Invalid notification type: ${notificationType}`, + ) + } + } + + async sendCivilClaimantNotification( + type: CivilClaimantNotificationType, + civilClaimant: CivilClaimant, + theCase: Case, + ): Promise { + await this.refreshFormatMessage() + + try { + return await this.sendNotification(type, civilClaimant, theCase) + } catch (error) { + this.logger.error('Failed to send notification', error) + + return { delivered: false } + } + } +} diff --git a/apps/judicial-system/backend/src/app/modules/notification/civilClaimantNotification.strings.ts b/apps/judicial-system/backend/src/app/modules/notification/civilClaimantNotification.strings.ts new file mode 100644 index 000000000000..d9beac015345 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/civilClaimantNotification.strings.ts @@ -0,0 +1,17 @@ +import { defineMessage } from '@formatjs/intl' + +export const strings = { + civilClaimantSpokespersonAssignedSubject: defineMessage({ + id: 'judicial.system.backend:civil_claimant_notifications.spokesperson_assigned_subject', + defaultMessage: '{courtName} - aðgangur að máli', + description: + 'Subject of the notification when a civil claimant spokesperson is assigned and confirmed', + }), + civilClaimantSpokespersonAssignedBody: defineMessage({ + id: 'judicial.system.backend:civil_claimant_notifications.indictment_assigned_body', + defaultMessage: + '{courtName} hefur skráð þig {spokespersonIsLawyer, select, true {lögmann einkaréttarkröfuhafa} other {réttargæslumann einkaréttarkröfuhafa}} í máli {courtCaseNumber}.

{spokespersonHasAccessToRVG, select, true {Sjá nánar á {linkStart}yfirlitssíðu málsins í Réttarvörslugátt{linkEnd}} other {Þú getur nálgast málið hjá dómstólnum.}}.', + description: + 'Body of the notification when a civil claimant spokesperson is assigned and confirmed', + }), +} diff --git a/apps/judicial-system/backend/src/app/modules/notification/dto/civilClaimantNotification.dto.ts b/apps/judicial-system/backend/src/app/modules/notification/dto/civilClaimantNotification.dto.ts new file mode 100644 index 000000000000..5557dbdfe354 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/dto/civilClaimantNotification.dto.ts @@ -0,0 +1,12 @@ +import { IsEnum, IsNotEmpty } from 'class-validator' + +import { ApiProperty } from '@nestjs/swagger' + +import { CivilClaimantNotificationType } from '@island.is/judicial-system/types' + +export class CivilClaimantNotificationDto { + @IsNotEmpty() + @IsEnum(CivilClaimantNotificationType) + @ApiProperty({ enum: CivilClaimantNotificationType }) + readonly type!: CivilClaimantNotificationType +} 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 53237a099dd2..c0e2ca1a0003 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 @@ -18,15 +18,24 @@ import { } from '@island.is/judicial-system/message' import { Case, CaseHasExistedGuard, CurrentCase } from '../case' -import { CurrentDefendant, Defendant, DefendantExistsGuard } from '../defendant' +import { + CivilClaimant, + CivilClaimantExistsGuard, + CurrentCivilClaimant, + CurrentDefendant, + Defendant, + DefendantExistsGuard, +} from '../defendant' import { SubpoenaExistsGuard } from '../subpoena' import { CaseNotificationDto } from './dto/caseNotification.dto' +import { CivilClaimantNotificationDto } from './dto/civilClaimantNotification.dto' import { DefendantNotificationDto } from './dto/defendantNotification.dto' import { InstitutionNotificationDto } from './dto/institutionNotification.dto' import { NotificationDispatchDto } from './dto/notificationDispatch.dto' import { SubpoenaNotificationDto } from './dto/subpoenaNotification.dto' import { DeliverResponse } from './models/deliver.response' import { CaseNotificationService } from './caseNotification.service' +import { CivilClaimantNotificationService } from './civilClaimantNotification.service' import { DefendantNotificationService } from './defendantNotification.service' import { InstitutionNotificationService } from './institutionNotification.service' import { NotificationDispatchService } from './notificationDispatch.service' @@ -42,6 +51,7 @@ export class InternalNotificationController { private readonly institutionNotificationService: InstitutionNotificationService, private readonly subpoenaNotificationService: SubpoenaNotificationService, private readonly defendantNotificationService: DefendantNotificationService, + private readonly civilClaimantNotificationService: CivilClaimantNotificationService, @Inject(LOGGER_PROVIDER) private readonly logger: Logger, ) {} @@ -123,6 +133,34 @@ export class InternalNotificationController { ) } + @Post( + `case/:caseId/${ + messageEndpoint[MessageType.CIVIL_CLAIMANT_NOTIFICATION] + }/:civilClaimantId`, + ) + @UseGuards(CaseHasExistedGuard, CivilClaimantExistsGuard) + @ApiCreatedResponse({ + type: DeliverResponse, + description: 'Sends civil claimant related notifications', + }) + sendCivilClaimantNotification( + @Param('caseId') caseId: string, + @Param('civilClaimantId') civilClaimantId: string, + @CurrentCase() theCase: Case, + @CurrentCivilClaimant() civilClaimant: CivilClaimant, + @Body() notificationDto: CivilClaimantNotificationDto, + ): Promise { + this.logger.debug( + `Sending ${notificationDto.type} notification for civil claimant ${civilClaimantId} and case ${caseId}`, + ) + + return this.civilClaimantNotificationService.sendCivilClaimantNotification( + notificationDto.type, + civilClaimant, + theCase, + ) + } + @Post(messageEndpoint[MessageType.NOTIFICATION_DISPATCH]) @ApiCreatedResponse({ type: DeliverResponse, 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 6dcb837ace05..14fb32c0a565 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 @@ -18,6 +18,7 @@ import { } from '../index' import { Notification } from './models/notification.model' import { CaseNotificationService } from './caseNotification.service' +import { CivilClaimantNotificationService } from './civilClaimantNotification.service' import { DefendantNotificationService } from './defendantNotification.service' import { InstitutionNotificationService } from './institutionNotification.service' import { InternalNotificationController } from './internalNotification.controller' @@ -43,12 +44,13 @@ import { SubpoenaNotificationService } from './subpoenaNotification.service' ], controllers: [NotificationController, InternalNotificationController], providers: [ - NotificationService, CaseNotificationService, - NotificationDispatchService, + CivilClaimantNotificationService, + DefendantNotificationService, InstitutionNotificationService, + NotificationService, + NotificationDispatchService, SubpoenaNotificationService, - DefendantNotificationService, ], }) export class NotificationModule {} 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 a269d181001b..267327b2235b 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 @@ -25,6 +25,7 @@ import { eventModuleConfig, EventService } from '../../event' import { InstitutionService } from '../../institution' import { UserService } from '../../user' import { CaseNotificationService } from '../caseNotification.service' +import { CivilClaimantNotificationService } from '../civilClaimantNotification.service' import { DefendantNotificationService } from '../defendantNotification.service' import { InstitutionNotificationService } from '../institutionNotification.service' import { InternalNotificationController } from '../internalNotification.controller' @@ -104,6 +105,7 @@ export const createTestingNotificationModule = async () => { NotificationDispatchService, InstitutionNotificationService, DefendantNotificationService, + CivilClaimantNotificationService, ], }) .useMocker((token) => { diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/civilClaimantNotification/sendSpokespersonAssignedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/civilClaimantNotification/sendSpokespersonAssignedNotifications.spec.ts new file mode 100644 index 000000000000..42bf7c2a0ea8 --- /dev/null +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/civilClaimantNotification/sendSpokespersonAssignedNotifications.spec.ts @@ -0,0 +1,167 @@ +import { uuid } from 'uuidv4' + +import { EmailService } from '@island.is/email-service' +import { ConfigType } from '@island.is/nest/config' + +import { DEFENDER_INDICTMENT_ROUTE } from '@island.is/judicial-system/consts' +import { + CaseType, + CivilClaimantNotificationType, +} from '@island.is/judicial-system/types' + +import { createTestingNotificationModule } from '../../createTestingNotificationModule' + +import { Case } from '../../../../case' +import { CivilClaimant } from '../../../../defendant' +import { CivilClaimantNotificationDto } from '../../../dto/civilClaimantNotification.dto' +import { DeliverResponse } from '../../../models/deliver.response' +import { Notification } from '../../../models/notification.model' +import { notificationModuleConfig } from '../../../notification.config' + +jest.mock('../../../../../factories') + +interface Then { + result: DeliverResponse + error: Error +} + +type GivenWhenThen = ( + caseId: string, + civilClaimantId: string, + theCase: Case, + civilClaimant: CivilClaimant, + notificationDto: CivilClaimantNotificationDto, +) => Promise + +describe('InternalNotificationController - Send spokesperson assigned notifications', () => { + const caseId = uuid() + const civilClaimantId = uuid() + const court = { name: 'Héraðsdómur Reykjavíkur' } as Case['court'] + + let mockEmailService: EmailService + let mockConfig: ConfigType + let mockNotificationModel: typeof Notification + let givenWhenThen: GivenWhenThen + + let civilClaimantNotificationDTO: CivilClaimantNotificationDto + + beforeEach(async () => { + const { + emailService, + notificationConfig, + internalNotificationController, + notificationModel, + } = await createTestingNotificationModule() + + civilClaimantNotificationDTO = { + type: CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, + } + + mockEmailService = emailService + mockConfig = notificationConfig + mockNotificationModel = notificationModel + + givenWhenThen = async ( + caseId: string, + civilClaimantId: string, + theCase: Case, + civilClaimant: CivilClaimant, + notificationDto: CivilClaimantNotificationDto, + ) => { + const then = {} as Then + + try { + then.result = + await internalNotificationController.sendCivilClaimantNotification( + caseId, + civilClaimantId, + theCase, + civilClaimant, + notificationDto, + ) + } catch (error) { + then.error = error as Error + } + + return then + } + }) + + describe.each([ + { isSpokespersonConfirmed: true, shouldSendEmail: true }, + { isSpokespersonConfirmed: false, shouldSendEmail: false }, + ])( + 'when sending a spokesperson assigned notification', + ({ isSpokespersonConfirmed, shouldSendEmail }) => { + const civilClaimant = { + id: civilClaimantId, + caseId, + isSpokespersonConfirmed, + spokespersonIsLawyer: true, + spokespersonNationalId: '1234567890', + spokespersonName: 'Ben 10', + spokespersonEmail: 'ben10@omnitrix.is', + } as CivilClaimant + + beforeEach(async () => { + await givenWhenThen( + caseId, + civilClaimantId, + { + id: caseId, + court, + courtCaseNumber: 'R-123-456', + type: CaseType.INDICTMENT, + civilClaimants: [civilClaimant], + hasCivilClaims: true, + } as Case, + civilClaimant, + civilClaimantNotificationDTO, + ) + }) + + test(`should ${ + shouldSendEmail ? '' : 'not ' + }send a spokesperson assigned notification`, async () => { + if (shouldSendEmail) { + expect(mockEmailService.sendEmail).toBeCalledTimes(1) + expect(mockEmailService.sendEmail).toBeCalledWith({ + from: { + name: mockConfig.email.fromName, + address: mockConfig.email.fromEmail, + }, + to: [ + { + name: civilClaimant.spokespersonName, + address: civilClaimant.spokespersonEmail, + }, + ], + replyTo: { + name: mockConfig.email.replyToName, + address: mockConfig.email.replyToEmail, + }, + attachments: undefined, + subject: `Héraðsdómur Reykjavíkur - aðgangur að máli`, + html: expect.stringContaining(DEFENDER_INDICTMENT_ROUTE), + text: expect.stringContaining( + `Héraðsdómur Reykjavíkur hefur skráð þig lögmann einkaréttarkröfuhafa í máli R-123-456`, + ), + }) + expect(mockNotificationModel.create).toHaveBeenCalledTimes(1) + expect(mockNotificationModel.create).toHaveBeenCalledWith({ + caseId, + type: civilClaimantNotificationDTO.type, + recipients: [ + { + address: civilClaimant.spokespersonEmail, + success: shouldSendEmail, + }, + ], + }) + } else { + expect(mockEmailService.sendEmail).not.toBeCalled() + } + }) + }, + ) +}) diff --git a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts index eff36227b407..d439505025a9 100644 --- a/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/notification/test/internalNotificationController/sendAdvocateAssignedNotifications.spec.ts @@ -83,120 +83,6 @@ describe('InternalNotificationController - Send defender assigned notifications' } }) - describe('when the case has civil claims and the advocate is a lawyer', () => { - const caseId = uuid() - const civilClaimant = { - hasSpokesperson: true, - spokespersonNationalId: '1234567890', - spokespersonEmail: 'recipient@gmail.com', - spokespersonName: 'John Doe', - spokespersonIsLawyer: true, - } - const theCase = { - id: caseId, - type: CaseType.INDICTMENT, - court, - courtCaseNumber: 'S-123/2022', - civilClaimants: [civilClaimant], - } as Case - - beforeEach(async () => { - await givenWhenThen(caseId, theCase, notificationDTO) - }) - - it('should send correct email', () => { - expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(1) - expect(mockEmailService.sendEmail).toHaveBeenCalledWith({ - from: { - name: mockConfig.email.fromName, - address: mockConfig.email.fromEmail, - }, - to: [ - { - name: civilClaimant.spokespersonName, - address: civilClaimant.spokespersonEmail, - }, - ], - replyTo: { - name: mockConfig.email.replyToName, - address: mockConfig.email.replyToEmail, - }, - attachments: undefined, - subject: `Skráning í máli ${theCase.courtCaseNumber}`, - text: expect.anything(), // same as html but stripped html tags - html: `Héraðsdómur Reykjavíkur hefur skráð þig lögmann einkaréttarkröfuhafa í máli ${theCase.courtCaseNumber}.

Sjá nánar á yfirlitssíðu málsins í Réttarvörslugátt.`, - }) - }) - }) - - describe('when the case has civil claims and the advocate is a legal rights protector', () => { - const caseId = uuid() - const civilClaimant = { - hasSpokesperson: true, - spokespersonNationalId: '1234567890', - spokespersonEmail: 'recipient@gmail.com', - spokespersonName: 'John Doe', - spokespersonIsLawyer: false, - } - const theCase = { - id: caseId, - type: CaseType.INDICTMENT, - court, - courtCaseNumber: 'S-123/2022', - civilClaimants: [civilClaimant], - } as Case - - beforeEach(async () => { - await givenWhenThen(caseId, theCase, notificationDTO) - }) - - it('should send correct email', () => { - expect(mockEmailService.sendEmail).toHaveBeenCalledTimes(1) - expect(mockEmailService.sendEmail).toHaveBeenCalledWith({ - from: { - name: mockConfig.email.fromName, - address: mockConfig.email.fromEmail, - }, - to: [ - { - name: civilClaimant.spokespersonName, - address: civilClaimant.spokespersonEmail, - }, - ], - replyTo: { - name: mockConfig.email.replyToName, - address: mockConfig.email.replyToEmail, - }, - attachments: undefined, - subject: `Skráning í máli ${theCase.courtCaseNumber}`, - text: expect.anything(), // same as html but stripped html tags - html: `Héraðsdómur Reykjavíkur hefur skráð þig réttargæslumann einkaréttarkröfuhafa í máli ${theCase.courtCaseNumber}.

Sjá nánar á yfirlitssíðu málsins í Réttarvörslugátt.`, - }) - }) - }) - - describe('when the case has civil claims and civil claimant does not have representation', () => { - const caseId = uuid() - const civilClaimant = { - hasSpokesperson: false, - } - const theCase = { - id: caseId, - type: CaseType.INDICTMENT, - court, - courtCaseNumber: 'S-123/2022', - civilClaimants: [civilClaimant], - } as Case - - beforeEach(async () => { - await givenWhenThen(caseId, theCase, notificationDTO) - }) - - it('should send correct email', () => { - expect(mockEmailService.sendEmail).not.toHaveBeenCalled() - }) - }) - describe('when sending assigned defender notifications in a restriction case', () => { const caseId = uuid() const theCase = { diff --git a/libs/judicial-system/message/src/lib/message.ts b/libs/judicial-system/message/src/lib/message.ts index 4cccc3b48615..c8650bc09256 100644 --- a/libs/judicial-system/message/src/lib/message.ts +++ b/libs/judicial-system/message/src/lib/message.ts @@ -30,6 +30,7 @@ export enum MessageType { INSTITUTION_NOTIFICATION = 'INSTITUTION_NOTIFICATION', NOTIFICATION_DISPATCH = 'NOTIFICATION_DISPATCH', DEFENDANT_NOTIFICATION = 'DEFENDANT_NOTIFICATION', + CIVIL_CLAIMANT_NOTIFICATION = 'CIVIL_CLAIMANT_NOTIFICATION', } export const messageEndpoint: { [key in MessageType]: string } = { @@ -66,6 +67,7 @@ export const messageEndpoint: { [key in MessageType]: string } = { INSTITUTION_NOTIFICATION: 'institutionNotification', NOTIFICATION_DISPATCH: 'notification/dispatch', DEFENDANT_NOTIFICATION: 'defendantNotification', + CIVIL_CLAIMANT_NOTIFICATION: 'civilClaimantNotification', } export type Message = { diff --git a/libs/judicial-system/types/src/index.ts b/libs/judicial-system/types/src/index.ts index 494e5067a5e2..4357ea6ad607 100644 --- a/libs/judicial-system/types/src/index.ts +++ b/libs/judicial-system/types/src/index.ts @@ -18,6 +18,7 @@ export { InstitutionNotificationType, NotificationDispatchType, DefendantNotificationType, + CivilClaimantNotificationType, 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 981ab499ff70..6c449ffc196e 100644 --- a/libs/judicial-system/types/src/lib/notification.ts +++ b/libs/judicial-system/types/src/lib/notification.ts @@ -30,6 +30,10 @@ export enum SubpoenaNotificationType { SERVICE_FAILED = 'SERVICE_FAILED', } +export enum CivilClaimantNotificationType { + SPOKESPERSON_ASSIGNED = 'SPOKESPERSON_ASSIGNED', +} + export enum InstitutionNotificationType { INDICTMENTS_WAITING_FOR_CONFIRMATION = 'INDICTMENTS_WAITING_FOR_CONFIRMATION', } @@ -58,6 +62,7 @@ export enum NotificationType { DEFENDER_ASSIGNED = DefendantNotificationType.DEFENDER_ASSIGNED, SERVICE_SUCCESSFUL = SubpoenaNotificationType.SERVICE_SUCCESSFUL, SERVICE_FAILED = SubpoenaNotificationType.SERVICE_FAILED, + SPOKESPERSON_ASSIGNED = CivilClaimantNotificationType.SPOKESPERSON_ASSIGNED, INDICTMENTS_WAITING_FOR_CONFIRMATION = InstitutionNotificationType.INDICTMENTS_WAITING_FOR_CONFIRMATION, }