From 162538d952cf1455b37bbafc3282f2aec980fe0b Mon Sep 17 00:00:00 2001 From: BushraAbdullahi <103956365+BushraAbdullahi@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:11:44 +0100 Subject: [PATCH] Add resolvedByUsername to case list query (#719) * update logic and write test * refactor logic and update test * refactor function to helper file * WIP * WIP * add test for formatName * WIP * complete test --- src/helpers/formatName.ts | 9 ++ src/services/listCourtCases.ts | 83 ++++++++++---- ...erByResolvedByUsername.integration.test.ts | 107 ++++++++++++++++++ .../listCourtCases/formatName.test.ts | 33 ++++++ .../listCourtCases.integration.test.ts | 1 + 5 files changed, 213 insertions(+), 20 deletions(-) create mode 100644 src/helpers/formatName.ts create mode 100644 test/services/listCourtCases/filterByResolvedByUsername.integration.test.ts create mode 100644 test/services/listCourtCases/formatName.test.ts diff --git a/src/helpers/formatName.ts b/src/helpers/formatName.ts new file mode 100644 index 000000000..62902ac7d --- /dev/null +++ b/src/helpers/formatName.ts @@ -0,0 +1,9 @@ +export function formatName(name: string) { + let splitName = name.replace(/\*|\s+/g, "%") + + if (!name.includes("%") && !splitName.endsWith("%")) { + splitName = `${splitName}%` + } + + return splitName +} diff --git a/src/services/listCourtCases.ts b/src/services/listCourtCases.ts index f07b7c2a1..2a3c4d7ad 100644 --- a/src/services/listCourtCases.ts +++ b/src/services/listCourtCases.ts @@ -3,7 +3,6 @@ import { CaseListQueryParams, LockedState } from "types/CaseListQueryParams" import { ListCourtCaseResult } from "types/ListCourtCasesResult" import Permission from "types/Permission" import PromiseResult from "types/PromiseResult" - import { isError } from "types/Result" import CourtCase from "./entities/CourtCase" import getLongTriggerCode from "./entities/transformers/getLongTriggerCode" @@ -11,6 +10,7 @@ import User from "./entities/User" import filterByReasonAndResolutionStatus from "./filters/filterByReasonAndResolutionStatus" import courtCasesByOrganisationUnitQuery from "./queries/courtCasesByOrganisationUnitQuery" import leftJoinAndSelectTriggersQuery from "./queries/leftJoinAndSelectTriggersQuery" +import { formatName } from "../helpers/formatName" const listCourtCases = async ( connection: DataSource, @@ -60,7 +60,9 @@ const listCourtCases = async ( "courtCase.errorLockedByUsername", "courtCase.triggerLockedByUsername" ]) + query = courtCasesByOrganisationUnitQuery(query, user) + leftJoinAndSelectTriggersQuery(query, user.excludedTriggers, caseState ?? "Unresolved") .leftJoinAndSelect("courtCase.notes", "note") .leftJoin("courtCase.errorLockedByUser", "errorLockedByUser") @@ -86,27 +88,31 @@ const listCourtCases = async ( // Filters if (defendantName) { - let splitDefendantName = defendantName.replace(/\*|\s+/g, "%") - - if (!splitDefendantName.endsWith("%")) { - splitDefendantName = `${splitDefendantName}%` - } + const splitDefendantName = formatName(defendantName) - query.andWhere({ defendantName: ILike(splitDefendantName) }) + query.andWhere({ + defendantName: ILike(splitDefendantName) + }) } if (courtName) { - const courtNameLike = { courtName: ILike(`%${courtName}%`) } + const courtNameLike = { + courtName: ILike(`%${courtName}%`) + } query.andWhere(courtNameLike) } if (ptiurn) { - const ptiurnLike = { ptiurn: ILike(`%${ptiurn}%`) } + const ptiurnLike = { + ptiurn: ILike(`%${ptiurn}%`) + } query.andWhere(ptiurnLike) } if (asn) { - query.andWhere({ asn: ILike(`%${asn}%`) }) + query.andWhere({ + asn: ILike(`%${asn}%`) + }) } if (reasonCodes?.length) { @@ -119,7 +125,7 @@ const listCourtCases = async ( .orWhere("courtCase.error_report ilike any(array[:...firstExceptions])", { firstExceptions: reasonCodes.map((reasonCode) => `${reasonCode}||%`) }) - // match exceptions ins the middle of the error report + // match exceptions in the middle of the error report .orWhere("courtCase.error_report ilike any(array[:...exceptions])", { exceptions: reasonCodes.map((reasonCode) => `% ${reasonCode}||%`) }) @@ -135,8 +141,12 @@ const listCourtCases = async ( qb.orWhere( new Brackets((dateRangeQuery) => { dateRangeQuery - .andWhere({ courtDate: MoreThanOrEqual(dateRange.from) }) - .andWhere({ courtDate: LessThanOrEqual(dateRange.to) }) + .andWhere({ + courtDate: MoreThanOrEqual(dateRange.from) + }) + .andWhere({ + courtDate: LessThanOrEqual(dateRange.to) + }) }) ) }) @@ -144,11 +154,30 @@ const listCourtCases = async ( ) } else { query - .andWhere({ courtDate: MoreThanOrEqual(courtDateRange.from) }) - .andWhere({ courtDate: LessThanOrEqual(courtDateRange.to) }) + .andWhere({ + courtDate: MoreThanOrEqual(courtDateRange.from) + }) + .andWhere({ + courtDate: LessThanOrEqual(courtDateRange.to) + }) } } + if (resolvedByUsername) { + const splitResolvedByUsername = formatName(resolvedByUsername) + + query.andWhere( + new Brackets((qb) => { + qb.where({ + errorResolvedBy: ILike(splitResolvedByUsername) + }).orWhere({ + triggerResolvedBy: ILike(splitResolvedByUsername) + }) + }) + ) + } + + // Existing filters filterByReasonAndResolutionStatus(query, user, reason, reasonCodes, caseState, resolvedByUsername) if (caseState === "Resolved" && resolvedDateRange) { @@ -160,7 +189,9 @@ const listCourtCases = async ( if (allocatedToUserName) { query.andWhere( new Brackets((qb) => { - qb.where({ errorLockedByUsername: allocatedToUserName }).orWhere({ + qb.where({ + errorLockedByUsername: allocatedToUserName + }).orWhere({ triggerLockedByUsername: allocatedToUserName }) }) @@ -171,13 +202,21 @@ const listCourtCases = async ( if (lockedState === LockedState.Locked) { query.andWhere( new Brackets((qb) => { - qb.where({ errorLockedByUsername: Not(IsNull()) }).orWhere({ triggerLockedByUsername: Not(IsNull()) }) + qb.where({ + errorLockedByUsername: Not(IsNull()) + }).orWhere({ + triggerLockedByUsername: Not(IsNull()) + }) }) ) } else if (lockedState === LockedState.Unlocked) { query.andWhere( new Brackets((qb) => { - qb.where({ errorLockedByUsername: IsNull() }).andWhere({ triggerLockedByUsername: IsNull() }) + qb.where({ + errorLockedByUsername: IsNull() + }).andWhere({ + triggerLockedByUsername: IsNull() + }) }) ) } @@ -188,11 +227,15 @@ const listCourtCases = async ( } if (!user.hasAccessTo[Permission.Triggers]) { - query.andWhere({ errorCount: MoreThan(0) }) + query.andWhere({ + errorCount: MoreThan(0) + }) } if (!user.hasAccessTo[Permission.Exceptions]) { - query.andWhere({ triggerCount: MoreThan(0) }) + query.andWhere({ + triggerCount: MoreThan(0) + }) } const result = await query.getManyAndCount().catch((error: Error) => error) diff --git a/test/services/listCourtCases/filterByResolvedByUsername.integration.test.ts b/test/services/listCourtCases/filterByResolvedByUsername.integration.test.ts new file mode 100644 index 000000000..f3d1d5937 --- /dev/null +++ b/test/services/listCourtCases/filterByResolvedByUsername.integration.test.ts @@ -0,0 +1,107 @@ +import CourtCase from "services/entities/CourtCase" +import User from "services/entities/User" +import getDataSource from "services/getDataSource" +import listCourtCases from "services/listCourtCases" +import courtCasesByOrganisationUnitQuery from "services/queries/courtCasesByOrganisationUnitQuery" +import leftJoinAndSelectTriggersQuery from "services/queries/leftJoinAndSelectTriggersQuery" +import { DataSource } from "typeorm" +import { ListCourtCaseResult } from "types/ListCourtCasesResult" +import { isError } from "../../../src/types/Result" +import { hasAccessToAll } from "../../helpers/hasAccessTo" +import deleteFromEntity from "../../utils/deleteFromEntity" +import { insertCourtCasesWithFields } from "../../utils/insertCourtCases" +import Trigger from "../../../src/services/entities/Trigger" +import Note from "services/entities/Note" + +jest.mock("services/queries/courtCasesByOrganisationUnitQuery") +jest.mock("services/queries/leftJoinAndSelectTriggersQuery") + +jest.setTimeout(100000) +describe("listCourtCases", () => { + let dataSource: DataSource + const forceCode = "036" + const testUser = { + visibleForces: [forceCode], + visibleCourts: [], + hasAccessTo: hasAccessToAll + } as Partial as User + + beforeAll(async () => { + dataSource = await getDataSource() + }) + + beforeEach(async () => { + await deleteFromEntity(CourtCase) + await deleteFromEntity(Trigger) + await deleteFromEntity(Note) + jest.resetAllMocks() + jest.clearAllMocks() + ;(courtCasesByOrganisationUnitQuery as jest.Mock).mockImplementation( + jest.requireActual("services/queries/courtCasesByOrganisationUnitQuery").default + ) + ;(leftJoinAndSelectTriggersQuery as jest.Mock).mockImplementation( + jest.requireActual("services/queries/leftJoinAndSelectTriggersQuery").default + ) + }) + + afterAll(async () => { + if (dataSource) { + await dataSource.destroy() + } + }) + + describe("search by resolvedByUsername", () => { + it("Should list cases that match the partial username search", async () => { + await insertCourtCasesWithFields([ + { errorResolvedBy: "User Name01" }, + { triggerResolvedBy: "User Name02" }, + { errorResolvedBy: "User Name03" } + ]) + + const result = await listCourtCases(dataSource, { maxPageItems: 100, resolvedByUsername: "User" }, testUser) + expect(isError(result)).toBe(false) + const { result: cases } = result as ListCourtCaseResult + + expect(cases).toHaveLength(3) + expect(cases[0].errorResolvedBy).toStrictEqual("User Name01") + expect(cases[1].triggerResolvedBy).toStrictEqual("User Name02") + expect(cases[2].errorResolvedBy).toStrictEqual("User Name03") + }) + + it("Should list cases that match the full username search", async () => { + await insertCourtCasesWithFields([ + { errorResolvedBy: "User Name01" }, + { triggerResolvedBy: "User Name02" }, + { errorResolvedBy: "User Name03" } + ]) + + const result = await listCourtCases( + dataSource, + { maxPageItems: 100, resolvedByUsername: "User Name01" }, + testUser + ) + expect(isError(result)).toBe(false) + const { result: cases } = result as ListCourtCaseResult + + expect(cases).toHaveLength(1) + expect(cases[0].errorResolvedBy).toStrictEqual("User Name01") + }) + + it("Should handle wildcard searches for partial usernames", async () => { + await insertCourtCasesWithFields([ + { errorResolvedBy: "User Name01" }, + { triggerResolvedBy: "User Name02" }, + { errorResolvedBy: "User Name03" } + ]) + + const result = await listCourtCases(dataSource, { maxPageItems: 100, resolvedByUsername: "%Name0%" }, testUser) + expect(isError(result)).toBe(false) + const { result: cases } = result as ListCourtCaseResult + + expect(cases).toHaveLength(3) + expect(cases[0].errorResolvedBy).toStrictEqual("User Name01") + expect(cases[1].triggerResolvedBy).toStrictEqual("User Name02") + expect(cases[2].errorResolvedBy).toStrictEqual("User Name03") + }) + }) +}) diff --git a/test/services/listCourtCases/formatName.test.ts b/test/services/listCourtCases/formatName.test.ts new file mode 100644 index 000000000..0c41f51b2 --- /dev/null +++ b/test/services/listCourtCases/formatName.test.ts @@ -0,0 +1,33 @@ +import { formatName } from "../../../src/helpers/formatName" + +describe("formatName", () => { + it('should replace spaces with "%" and add "%" at the end', () => { + expect(formatName("John Doe")).toBe("John%Doe%") + }) + + it('should replace "*" with "%" and add "%" at the end', () => { + expect(formatName("John*Doe")).toBe("John%Doe%") + }) + + it('should not add another "%" if already present at the end', () => { + expect(formatName("John Doe%")).toBe("John%Doe%") + }) + + it('should add "%" if there are no spaces or special characters', () => { + expect(formatName("John")).toBe("John%") + }) + + it("should handle empty string", () => { + expect(formatName("")).toBe("%") + }) + + it("should handle a wildcard character anywhere in the string", () => { + expect(formatName("Name0%")).toBe("Name0%") + expect(formatName("User%Name")).toBe("User%Name") + expect(formatName("%Name")).toBe("%Name") + }) + + it('should still replace spaces and append "%" at the end if no wildcard is present', () => { + expect(formatName("Name 0")).toBe("Name%0%") + }) +}) diff --git a/test/services/listCourtCases/listCourtCases.integration.test.ts b/test/services/listCourtCases/listCourtCases.integration.test.ts index 6909dcb86..d2ce03fcc 100644 --- a/test/services/listCourtCases/listCourtCases.integration.test.ts +++ b/test/services/listCourtCases/listCourtCases.integration.test.ts @@ -539,6 +539,7 @@ describe("listCourtCases", () => { expect(cases[1].ptiurn).toStrictEqual(ptiurnToIncludeWithPartialMatch) }) }) + describe("search by reason", () => { it("Should list cases when there is a case insensitive match in triggers or exceptions", async () => { await insertCourtCasesWithFields(Array.from({ length: 4 }, () => ({ orgForPoliceFilter: orgCode })))