Skip to content

Commit

Permalink
feat(j-s): Get connected Indictment cases to merge with (#15508)
Browse files Browse the repository at this point in the history
* feat(j-s): Get connected indictment cases

* fix(j-s): Added guards and roles tests

* fix(j-s): Review comment

* test(j-s): Added test for getConnectedCases

* fix(j-s): AuditedAction type

* fix(j-s): Update type

* fix(j-s): Cleanup

* fix(j-s): More cleanup

* fix(j-s): Handle more than one defendant

* fix(j-s): Merge conflict
  • Loading branch information
unakb authored Jul 15, 2024
1 parent 7cb58ad commit 143c0e0
Show file tree
Hide file tree
Showing 23 changed files with 446 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,10 @@ export class BackendService extends DataSource<{ req: Request }> {
return this.get<Case>(`case/${id}`, this.caseTransformer)
}

getConnectedCases(id: string): Promise<Case[]> {
return this.get(`case/${id}/connectedCases`)
}

createCase(createCase: unknown): Promise<Case> {
return this.post<unknown, Case>('case', createCase, this.caseTransformer)
}
Expand Down
19 changes: 19 additions & 0 deletions apps/judicial-system/api/src/app/modules/case/case.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,25 @@ export class CaseResolver {
)
}

@Query(() => [Case], { nullable: true })
async connectedCases(
@Args('input', { type: () => CaseQueryInput })
input: CaseQueryInput,
@CurrentGraphQlUser()
user: User,
@Context('dataSources')
{ backendService }: { backendService: BackendService },
): Promise<Case[]> {
this.logger.debug('Getting connected cases')

return this.auditTrailService.audit(
user.id,
AuditedAction.GET_CONNECTED_CASES,
backendService.getConnectedCases(input.id),
input.id,
)
}

@Mutation(() => Case, { nullable: true })
@UseInterceptors(CaseInterceptor)
createCase(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -501,4 +501,9 @@ export class UpdateCaseInput {
@IsOptional()
@Field(() => CourtSessionType, { nullable: true })
readonly courtSessionType?: CourtSessionType

@Allow()
@IsOptional()
@Field(() => ID, { nullable: true })
readonly mergeCaseId?: string
}
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ export class CaseController {
update.appealRulingModifiedHistory = `${history}${today} - ${user.name} ${user.title}\n\n${update.appealRulingModifiedHistory}`
}

if (update.mergeCaseId && theCase.state !== CaseState.RECEIVED) {
throw new BadRequestException(
'Cannot merge case that is not in a received state',
)
}

return this.caseService.update(theCase, update, user) as Promise<Case> // Never returns undefined
}

Expand Down Expand Up @@ -461,6 +467,33 @@ export class CaseController {
return theCase
}

@UseGuards(JwtAuthGuard, CaseExistsGuard)
@RolesRules(
districtCourtJudgeRule,
districtCourtRegistrarRule,
districtCourtAssistantRule,
)
@Get('case/:caseId/connectedCases')
@ApiOkResponse({ type: [Case], description: 'Gets all connected cases' })
async getConnectedCases(
@Param('caseId') caseId: string,
@CurrentCase() theCase: Case,
): Promise<Case[]> {
this.logger.debug(`Getting connected cases for case ${caseId}`)

if (!theCase.defendants || theCase.defendants.length === 0) {
return []
}

const connectedCases = await Promise.all(
theCase.defendants.map((defendant) =>
this.caseService.getConnectedIndictmentCases(theCase.id, defendant),
),
)

return connectedCases.flat()
}

@UseGuards(
JwtAuthGuard,
RolesGuard,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Op } from 'sequelize'
import { Op, WhereOptions } from 'sequelize'
import { Includeable, OrderItem, Transaction } from 'sequelize/types'
import { Sequelize } from 'sequelize-typescript'

Expand All @@ -20,6 +20,7 @@ import type { Logger } from '@island.is/logging'
import { LOGGER_PROVIDER } from '@island.is/logging'
import { type ConfigType } from '@island.is/nest/config'

import { formatNationalId } from '@island.is/judicial-system/formatters'
import {
CaseMessage,
MessageService,
Expand Down Expand Up @@ -162,6 +163,7 @@ export interface UpdateCase
| 'rulingSignatureDate'
| 'judgeId'
| 'courtSessionType'
| 'mergeCaseId'
> {
type?: CaseType
state?: CaseState
Expand Down Expand Up @@ -1319,6 +1321,49 @@ export class CaseService {
})
}

getConnectedIndictmentCases(
caseId: string,
defendant: Defendant,
): Promise<Case[]> {
let whereClause: WhereOptions = {
type: CaseType.INDICTMENT,
id: { [Op.ne]: caseId },
state: [CaseState.RECEIVED],
}

if (defendant.noNationalId) {
whereClause = {
...whereClause,
[Op.and]: [
{ '$defendants.national_id$': defendant.nationalId },
{ '$defendants.name$': defendant.name },
],
}
} else {
const formattedNationalId = formatNationalId(defendant.nationalId)
whereClause = {
...whereClause,
[Op.or]: [
{ '$defendants.national_id$': defendant.nationalId },
{ '$defendants.national_id$': formattedNationalId },
],
}
}

return this.caseModel.findAll({
include: [
{ model: Defendant, as: 'defendants' },
{
model: DateLog,
as: 'dateLogs',
},
],
order: [[{ model: DateLog, as: 'dateLogs' }, 'created', 'DESC']],
attributes: ['id', 'courtCaseNumber', 'type', 'state'],
where: whereClause,
})
}

async create(caseToCreate: CreateCaseDto, user: TUser): Promise<Case> {
return this.sequelize
.transaction(async (transaction) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,4 +508,9 @@ export class UpdateCaseDto {
@IsEnum(CourtSessionType)
@ApiPropertyOptional({ enum: CourtSessionType })
readonly courtSessionType?: CourtSessionType

@IsOptional()
@IsUUID()
@ApiPropertyOptional({ type: String })
readonly mergeCaseId?: string
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const districtCourtFields: (keyof UpdateCaseDto)[] = [
'indictmentRulingDecision',
'indictmentDecision',
'courtSessionType',
'mergeCaseId',
]

const courtOfAppealsFields: (keyof UpdateCaseDto)[] = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { uuid } from 'uuidv4'

import { User } from '@island.is/judicial-system/types'

import { createTestingCaseModule } from '../createTestingCaseModule'

import { Case } from '../../models/case.model'

interface Then {
result: Case[]
error: Error
}

type GivenWhenThen = (caseId: string, theCase: Case) => Promise<Then>

describe('CaseController - Get connected cases', () => {
const user = { id: uuid() } as User
let mockCaseModel: typeof Case
let givenWhenThen: GivenWhenThen

beforeEach(async () => {
const { caseModel, caseController } = await createTestingCaseModule()
mockCaseModel = caseModel

givenWhenThen = async (caseId: string, theCase: Case) => {
const then = {} as Then

try {
then.result = await caseController.getConnectedCases(caseId, theCase)
} catch (error) {
then.error = error as Error
}

return then
}
})

describe('case has no defendants', () => {
const caseId = uuid()
const theCase = { id: caseId } as Case
let then: Then

beforeEach(async () => {
then = await givenWhenThen(caseId, theCase)
})

it('should return an empty array', () => {
expect(then.result).toEqual([])
})
})

describe('case has defendants', () => {
const caseId = uuid()
const defendantId1 = uuid()
const defendantId2 = uuid()
const theCase = {
id: caseId,
defendants: [{ id: defendantId1 }, { id: defendantId2 }],
} as Case
const connectedCase1 = { id: uuid() } as Case
const connectedCase2 = { id: uuid() } as Case
let then: Then

beforeEach(async () => {
const mockFindAll = mockCaseModel.findAll as jest.Mock
mockFindAll.mockResolvedValueOnce([connectedCase1, connectedCase2])
then = await givenWhenThen(caseId, theCase)
})

it('should return the connected cases', () => {
expect(then.result).toEqual([connectedCase1, connectedCase2])
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { JwtAuthGuard } from '@island.is/judicial-system/auth'

import { CaseController } from '../../case.controller'
import { CaseExistsGuard } from '../../guards/caseExists.guard'

describe('CaseController - Get connected cases by case id guards', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let guards: any[]

beforeEach(() => {
guards = Reflect.getMetadata(
'__guards__',
CaseController.prototype.getConnectedCases,
)
})

it('should have the right guard configuration', () => {
expect(new guards[0]()).toBeInstanceOf(JwtAuthGuard)
expect(new guards[1]()).toBeInstanceOf(CaseExistsGuard)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {
districtCourtAssistantRule,
districtCourtJudgeRule,
districtCourtRegistrarRule,
} from '../../../../guards'
import { CaseController } from '../../case.controller'

describe('CaseController - Get connected cases by case id rules', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let rules: any[]

beforeEach(() => {
rules = Reflect.getMetadata(
'roles-rules',
CaseController.prototype.getConnectedCases,
)
})

it('should give permission to roles', () => {
expect(rules).toHaveLength(3)
expect(rules).toContain(districtCourtJudgeRule)
expect(rules).toContain(districtCourtRegistrarRule)
expect(rules).toContain(districtCourtAssistantRule)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,11 @@ query Case($input: CaseQueryInput!) {
courtSessionType
mergeCase {
id
courtCaseNumber
}
mergedCases {
id
courtCaseNumber
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,14 @@ const InfoCardClosedIndictment: FC<Props> = (props) => {
title: formatMessage(core.prosecutor),
value: `${workingCase.prosecutorsOffice?.name}`,
},
...(workingCase.mergeCase
? [
{
title: formatMessage(strings.indictmentMergedTitle),
value: workingCase.mergeCase?.courtCaseNumber,
},
]
: []),
{
title: formatMessage(core.court),
value: workingCase.court?.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,9 @@ export const strings = defineMessages({
defaultMessage: 'Dagsetning áritunar',
description: 'Notaður sem titill á "Dagsetning" hluta af yfirliti ákæru.',
},
indictmentMergedTitle: {
id: 'judicial.system.core:info_card_indictment.indictment_merged_title',
defaultMessage: 'Sameinað máli',
description: 'Notaður sem titill á "Sameinað" hluta af yfirliti ákæru.',
},
})
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,9 @@ export const strings = defineMessages({
description:
'Notaður sem skýritexti í tegund þinghalds valkosti á Niðurstaða skrefi.',
},
connectedCaseNumbersTitle: {
id: 'judicial.system.core:court.indictments.conclusion.connected_case_numbers_title',
defaultMessage: 'Málsnúmer',
description: 'Notaður sem titill á tengd mál textabox á Niðurstaða skrefi.',
},
})
Loading

0 comments on commit 143c0e0

Please sign in to comment.