diff --git a/apps/judicial-system/backend/src/app/modules/case/case.service.ts b/apps/judicial-system/backend/src/app/modules/case/case.service.ts index ddd76c7772d4..5c9f66ea866d 100644 --- a/apps/judicial-system/backend/src/app/modules/case/case.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/case.service.ts @@ -20,7 +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 { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' import { Message, MessageService, @@ -1355,42 +1355,46 @@ export class CaseService { caseId: string, defendant: Defendant, ): Promise { - let whereClause: WhereOptions = { - type: CaseType.INDICTMENT, - id: { [Op.ne]: caseId }, - state: [CaseState.RECEIVED], - } + const whereClause: WhereOptions = [ + { isArchived: false }, + { 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 }, - ], - } + whereClause.push({ + id: { + [Op.in]: Sequelize.literal(` + (SELECT case_id + FROM defendant + WHERE national_id = '${defendant.nationalId}' + AND name = '${defendant.name}' + )`), + }, + }) } else { - const formattedNationalId = formatNationalId(defendant.nationalId) - whereClause = { - ...whereClause, - [Op.or]: [ - { '$defendants.national_id$': defendant.nationalId }, - { '$defendants.national_id$': formattedNationalId }, - ], - } + const [normalizedNationalId, formattedNationalId] = + normalizeAndFormatNationalId(defendant.nationalId) + + whereClause.push({ + id: { + [Op.in]: Sequelize.literal(` + (SELECT case_id + FROM defendant + WHERE national_id in ('${normalizedNationalId}', '${formattedNationalId}') + )`), + }, + }) } return this.caseModel.findAll({ include: [ + { model: Institution, as: 'court' }, { model: Defendant, as: 'defendants' }, - { - model: DateLog, - as: 'dateLogs', - }, ], - order: [[{ model: DateLog, as: 'dateLogs' }, 'created', 'DESC']], attributes: ['id', 'courtCaseNumber', 'type', 'state'], - where: whereClause, + where: { [Op.and]: whereClause }, }) } diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts b/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts index 64c00f550136..dfb9f7bc678f 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/case.filter.ts @@ -1,4 +1,4 @@ -import { formatNationalId } from '@island.is/judicial-system/formatters' +import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' import type { User } from '@island.is/judicial-system/types' import { CaseAppealState, @@ -330,22 +330,27 @@ const canDefenceUserAccessCase = (theCase: Case, user: User): boolean => { } } - const formattedNationalId = formatNationalId(user.nationalId) + const normalizedAndFormattedNationalId = normalizeAndFormatNationalId( + user.nationalId, + ) + // Check case defender access if (isIndictmentCase(theCase.type)) { if ( !theCase.defendants?.some( (defendant) => - defendant.defenderNationalId === user.nationalId || - defendant.defenderNationalId === formattedNationalId, + defendant.defenderNationalId && + normalizedAndFormattedNationalId.includes( + defendant.defenderNationalId, + ), ) ) { return false } } else { if ( - theCase.defenderNationalId !== user.nationalId && - theCase.defenderNationalId !== formattedNationalId + !theCase.defenderNationalId || + !normalizedAndFormattedNationalId.includes(theCase.defenderNationalId) ) { return false } diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts b/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts index c9fa79cc834b..0ccb2aa3c953 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/cases.filter.ts @@ -2,7 +2,7 @@ import { Op, Sequelize, WhereOptions } from 'sequelize' import { ForbiddenException } from '@nestjs/common' -import { formatNationalId } from '@island.is/judicial-system/formatters' +import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' import type { User } from '@island.is/judicial-system/types' import { CaseAppealState, @@ -216,7 +216,9 @@ const getPrisonAdminUserCasesQueryFilter = (): WhereOptions => { } const getDefenceUserCasesQueryFilter = (user: User): WhereOptions => { - const formattedNationalId = formatNationalId(user.nationalId) + const [normalizedNationalId, formattedNationalId] = + normalizeAndFormatNationalId(user.nationalId) + const options: WhereOptions = [ { is_archived: false }, { @@ -238,6 +240,8 @@ const getDefenceUserCasesQueryFilter = (user: User): WhereOptions => { { [Op.and]: [ { state: CaseState.RECEIVED }, + // The following condition will filter out all date logs that are not of type ARRAIGNMENT_DATE + // but that should be ok for request cases since they only have one date log { '$dateLogs.date_type$': DateType.ARRAIGNMENT_DATE }, ], }, @@ -251,7 +255,7 @@ const getDefenceUserCasesQueryFilter = (user: User): WhereOptions => { ], }, { - defender_national_id: [user.nationalId, formattedNationalId], + defender_national_id: [normalizedNationalId, formattedNationalId], }, ], }, @@ -266,10 +270,13 @@ const getDefenceUserCasesQueryFilter = (user: User): WhereOptions => { ], }, { - '$defendants.defender_national_id$': [ - user.nationalId, - formattedNationalId, - ], + id: { + [Op.in]: Sequelize.literal(` + (SELECT case_id + FROM defendant + WHERE defender_national_id in ('${normalizedNationalId}', '${formattedNationalId}')) + `), + }, }, ], }, diff --git a/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts b/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts index 1c412b863bd7..0e2217081425 100644 --- a/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/filters/test/cases.filter.spec.ts @@ -452,10 +452,13 @@ describe('getCasesQueryFilter', () => { ], }, { - '$defendants.defender_national_id$': [ - user.nationalId, - user.nationalId, - ], + id: { + [Op.in]: Sequelize.literal(` + (SELECT case_id + FROM defendant + WHERE defender_national_id in ('${user.nationalId}', '${user.nationalId}')) + `), + }, }, ], }, 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 741699620065..ae2fb90f556e 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 @@ -20,7 +20,7 @@ import type { ConfigType } from '@island.is/nest/config' import { formatCaseType, - formatNationalId, + normalizeAndFormatNationalId, } from '@island.is/judicial-system/formatters' import { CaseFileCategory, @@ -1202,8 +1202,6 @@ export class InternalCaseService { // As this is only currently used by the digital mailbox API // we will only return indictment cases that have a court date async getIndictmentCases(nationalId: string): Promise { - const formattedNationalId = formatNationalId(nationalId) - return this.caseModel.findAll({ include: [ { model: Defendant, as: 'defendants' }, @@ -1220,19 +1218,14 @@ export class InternalCaseService { attributes: ['id', 'courtCaseNumber', 'type', 'state'], where: { type: CaseType.INDICTMENT, - [Op.or]: [ - { '$defendants.national_id$': nationalId }, - { '$defendants.national_id$': formattedNationalId }, - ], + // The national id could be without a hyphen or with a hyphen so we need to + // search for both + '$defendants.national_id$': normalizeAndFormatNationalId(nationalId), }, }) } async getIndictmentCase(caseId: string, nationalId: string): Promise { - // The national id could be without a hyphen or with a hyphen so we need to - // search for both - const formattedNationalId = formatNationalId(nationalId) - const caseById = await this.caseModel.findOne({ include: [ { model: Defendant, as: 'defendants' }, @@ -1246,10 +1239,9 @@ export class InternalCaseService { where: { type: CaseType.INDICTMENT, id: caseId, - [Op.or]: [ - { '$defendants.national_id$': nationalId }, - { '$defendants.national_id$': formattedNationalId }, - ], + // The national id could be without a hyphen or with a hyphen so we need to + // search for both + '$defendants.national_id$': normalizeAndFormatNationalId(nationalId), }, }) diff --git a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts index aa51cb9a6cda..1f9b75a06b9f 100644 --- a/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts +++ b/apps/judicial-system/backend/src/app/modules/case/limitedAccessCase.service.ts @@ -13,7 +13,7 @@ import { InjectModel } from '@nestjs/sequelize' import type { Logger } from '@island.is/logging' import { LOGGER_PROVIDER } from '@island.is/logging' -import { formatNationalId } from '@island.is/judicial-system/formatters' +import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' import { MessageService, MessageType } from '@island.is/judicial-system/message' import type { User as TUser } from '@island.is/judicial-system/types' import { @@ -363,14 +363,10 @@ export class LimitedAccessCaseService { } async findDefenderByNationalId(nationalId: string): Promise { - const formattedNationalId = formatNationalId(nationalId) return this.caseModel .findOne({ where: { - [Op.or]: [ - { defenderNationalId: formattedNationalId }, - { defenderNationalId: nationalId }, - ], + defenderNationalId: normalizeAndFormatNationalId(nationalId), state: { [Op.not]: CaseState.DELETED }, isArchived: false, }, diff --git a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/findDefenderByNationalId.spec.ts b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/findDefenderByNationalId.spec.ts index 7979e7477454..d2fb64ec33ff 100644 --- a/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/findDefenderByNationalId.spec.ts +++ b/apps/judicial-system/backend/src/app/modules/case/test/limitedAccessCaseController/findDefenderByNationalId.spec.ts @@ -75,10 +75,7 @@ describe('LimitedAccessCaseController - Find defender by national id', () => { it('should look for defender', () => { expect(mockCaseModel.findOne).toHaveBeenCalledWith({ where: { - [Op.or]: [ - { defenderNationalId: formattedDefenderNationalId }, - { defenderNationalId: defenderNationalId }, - ], + defenderNationalId: [defenderNationalId, formattedDefenderNationalId], state: { [Op.not]: CaseState.DELETED }, isArchived: false, }, diff --git a/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts b/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts index aa71527fd435..f323632f2476 100644 --- a/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts +++ b/apps/judicial-system/backend/src/app/modules/defendant/defendant.service.ts @@ -11,7 +11,7 @@ import { InjectModel } from '@nestjs/sequelize' import type { Logger } from '@island.is/logging' import { LOGGER_PROVIDER } from '@island.is/logging' -import { formatNationalId } from '@island.is/judicial-system/formatters' +import { normalizeAndFormatNationalId } from '@island.is/judicial-system/formatters' import { Message, MessageService, @@ -201,17 +201,12 @@ export class DefendantService { defendantNationalId: string, update: UpdateDefendantDto, ): Promise { - const formattedNationalId = formatNationalId(defendantNationalId) - const [numberOfAffectedRows, defendants] = await this.defendantModel.update( update, { where: { caseId, - [Op.or]: [ - { national_id: formattedNationalId }, - { national_id: defendantNationalId }, - ], + national_id: normalizeAndFormatNationalId(defendantNationalId), }, returning: true, }, diff --git a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts index a39933df0f9f..ffbacab59c49 100644 --- a/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts +++ b/apps/judicial-system/digital-mailbox-api/src/app/modules/cases/models/subpoena.response.ts @@ -4,7 +4,7 @@ import { ApiProperty } from '@nestjs/swagger' import { formatDate, - formatNationalId, + normalizeAndFormatNationalId, } from '@island.is/judicial-system/formatters' import { DateType, DefenderChoice } from '@island.is/judicial-system/types' @@ -47,13 +47,14 @@ export class SubpoenaResponse { defendantNationalId: string, lang?: string, ): SubpoenaResponse { - const formattedNationalId = formatNationalId(defendantNationalId) const t = getTranslations(lang) const defendantInfo = internalCase.defendants.find( (defendant) => - defendant.nationalId === formattedNationalId || - defendant.nationalId === defendantNationalId, + defendant.nationalId && + normalizeAndFormatNationalId(defendantNationalId).includes( + defendant.nationalId, + ), ) const waivedRight = defendantInfo?.defenderChoice === DefenderChoice.WAIVE diff --git a/apps/judicial-system/web/src/components/CourtArrangements/CourtArrangements.tsx b/apps/judicial-system/web/src/components/CourtArrangements/CourtArrangements.tsx index 2e02d96fbad2..9e8ee4794428 100644 --- a/apps/judicial-system/web/src/components/CourtArrangements/CourtArrangements.tsx +++ b/apps/judicial-system/web/src/components/CourtArrangements/CourtArrangements.tsx @@ -81,7 +81,7 @@ export const useCourtArrangements = ( setCourtDate((previous) => ({ ...previous, - date: date ? formatDateForServer(date) : null, + date: date ? date.toISOString() : null, })) } diff --git a/apps/judicial-system/web/src/routes/Court/Indictments/Conclusion/SelectConnectedCase.tsx b/apps/judicial-system/web/src/routes/Court/Indictments/Conclusion/SelectConnectedCase.tsx index 40f108d5d000..8c09daeb2ccd 100644 --- a/apps/judicial-system/web/src/routes/Court/Indictments/Conclusion/SelectConnectedCase.tsx +++ b/apps/judicial-system/web/src/routes/Court/Indictments/Conclusion/SelectConnectedCase.tsx @@ -41,10 +41,18 @@ const SelectConnectedCase: FC = ({ workingCase, setWorkingCase }) => { })) } - const connectedCases = connectedCasesData?.connectedCases?.map((aCase) => ({ - label: `${aCase.courtCaseNumber}`, - value: aCase.id, - })) as ConnectedCaseOption[] + const connectedCases = connectedCasesData?.connectedCases + ?.filter( + // The filtering is done here rather than on the server side + // as we may allow more relaxed merging later on + (connectedCase) => + connectedCase.defendants?.length === 1 && + connectedCase.court?.id === workingCase.court?.id, + ) + ?.map((connectedCase) => ({ + label: `${connectedCase.courtCaseNumber}`, + value: connectedCase.id, + })) as ConnectedCaseOption[] // For now we only want to allow cases with a single defendant to be able to merge // in to another case diff --git a/libs/judicial-system/formatters/src/lib/formatters.spec.ts b/libs/judicial-system/formatters/src/lib/formatters.spec.ts index a97d6ea41e74..45e9a52f7cc9 100644 --- a/libs/judicial-system/formatters/src/lib/formatters.spec.ts +++ b/libs/judicial-system/formatters/src/lib/formatters.spec.ts @@ -16,6 +16,7 @@ import { formatNationalId, formatPhoneNumber, indictmentSubtypes, + normalizeAndFormatNationalId, readableIndictmentSubtypes, sanitize, splitStringByComma, @@ -219,6 +220,30 @@ describe('formatNationalId', () => { }) }) +describe('normalizeAndFormatNationalId', () => { + test('should format national id with dash', () => { + // Arrange + const nationalId = '123456-7890' + + // Act + const res = normalizeAndFormatNationalId(nationalId) + + // Assert + expect(res).toEqual(['1234567890', '123456-7890']) + }) + + test('should format national id without dash', () => { + // Arrange + const nationalId = '1234567890' + + // Act + const res = normalizeAndFormatNationalId(nationalId) + + // Assert + expect(res).toEqual(['1234567890', '123456-7890']) + }) +}) + describe('formatDOB', () => { it('should format a national id string for a valid national id', () => { // Arrange diff --git a/libs/judicial-system/formatters/src/lib/formatters.ts b/libs/judicial-system/formatters/src/lib/formatters.ts index a25433fc0ffd..c54b451091f1 100644 --- a/libs/judicial-system/formatters/src/lib/formatters.ts +++ b/libs/judicial-system/formatters/src/lib/formatters.ts @@ -76,6 +76,12 @@ export const formatNationalId = (nationalId?: string | null): string => { } } +export const normalizeAndFormatNationalId = ( + nationalId?: string | null, +): [string, string] => { + return [nationalId?.replace(/-/g, '') ?? '', formatNationalId(nationalId)] +} + export const getInitials = (name?: string | null): string | undefined => { if (!name?.trim()) return undefined