diff --git a/backend/functions/lib/adapters/gateways/mongo/case-assignment.mongo.repository.test.ts b/backend/functions/lib/adapters/gateways/mongo/case-assignment.mongo.repository.test.ts index 1be9f9166..28a67ad37 100644 --- a/backend/functions/lib/adapters/gateways/mongo/case-assignment.mongo.repository.test.ts +++ b/backend/functions/lib/adapters/gateways/mongo/case-assignment.mongo.repository.test.ts @@ -55,9 +55,9 @@ describe('offices repo', () => { MockData.getAttorneyAssignment({ caseId }), ]; jest.spyOn(MongoCollectionAdapter.prototype, 'find').mockResolvedValue(mockAssignments); - const actualAssignment = await repo.findAssignmentsByCaseId(caseId); + const actualAssignment = await repo.findAssignmentsByCaseId([caseId]); - expect(actualAssignment).toEqual(mockAssignments); + expect(actualAssignment).toEqual(new Map([[caseId, mockAssignments]])); }); }); @@ -105,7 +105,7 @@ describe('offices repo', () => { test('should handle error on findAssignmentsByCaseId', async () => { const caseId = '111-22-33333'; jest.spyOn(MongoCollectionAdapter.prototype, 'find').mockRejectedValue(error); - await expect(async () => await repo.findAssignmentsByCaseId(caseId)).rejects.toThrow( + await expect(async () => await repo.findAssignmentsByCaseId([caseId])).rejects.toThrow( getCamsError( error, 'MONGO_COSMOS_DB_REPOSITORY_ASSIGNMENTS', diff --git a/backend/functions/lib/adapters/gateways/mongo/case-assignment.mongo.repository.ts b/backend/functions/lib/adapters/gateways/mongo/case-assignment.mongo.repository.ts index d63ed8f3b..cc83b7395 100644 --- a/backend/functions/lib/adapters/gateways/mongo/case-assignment.mongo.repository.ts +++ b/backend/functions/lib/adapters/gateways/mongo/case-assignment.mongo.repository.ts @@ -8,7 +8,7 @@ import { BaseMongoRepository } from './utils/base-mongo-repository'; const MODULE_NAME: string = 'CASE_ASSIGNMENT_MONGO_REPOSITORY'; const COLLECTION_NAME = 'assignments'; -const { and, equals, exists } = QueryBuilder; +const { and, equals, exists, contains } = QueryBuilder; export class CaseAssignmentMongoRepository extends BaseMongoRepository @@ -35,16 +35,28 @@ export class CaseAssignmentMongoRepository } } - async findAssignmentsByCaseId(caseId: string): Promise { + async findAssignmentsByCaseId(caseIds: string[]): Promise> { const query = QueryBuilder.build( and( equals('documentType', 'ASSIGNMENT'), - equals('caseId', caseId), + contains('caseId', caseIds), exists('unassignedOn', false), ), ); try { - return await this.getAdapter().find(query); + const assignments = await this.getAdapter().find(query); + const assignmentsMap = new Map(); + assignments.forEach((assignment) => { + if (assignmentsMap.has(assignment.caseId)) { + assignmentsMap.set(assignment.caseId, [ + ...assignmentsMap.get(assignment.caseId), + assignment, + ]); + } else { + assignmentsMap.set(assignment.caseId, [assignment]); + } + }); + return assignmentsMap; } catch (originalError) { throw getCamsError(originalError, MODULE_NAME, 'Unable to retrieve assignment.'); } diff --git a/backend/functions/lib/adapters/gateways/mongo/offices.mongo.repository.ts b/backend/functions/lib/adapters/gateways/mongo/offices.mongo.repository.ts index 4aa9a098d..d6abb23b6 100644 --- a/backend/functions/lib/adapters/gateways/mongo/offices.mongo.repository.ts +++ b/backend/functions/lib/adapters/gateways/mongo/offices.mongo.repository.ts @@ -1,5 +1,5 @@ import { ApplicationContext } from '../../types/basic'; -import { AttorneyUser, CamsUserReference } from '../../../../../../common/src/cams/users'; +import { AttorneyUser, CamsUserReference, Staff } from '../../../../../../common/src/cams/users'; import { Auditable, createAuditRecord } from '../../../../../../common/src/cams/auditable'; import { CamsRole } from '../../../../../../common/src/cams/roles'; import { getCamsUserReference } from '../../../../../../common/src/cams/session'; @@ -13,7 +13,7 @@ const COLLECTION_NAME = 'offices'; const { and, equals, contains } = QueryBuilder; -export type OfficeStaff = CamsUserReference & +export type OfficeStaff = Staff & Auditable & { documentType: 'OFFICE_STAFF'; officeCode: string; diff --git a/backend/functions/lib/controllers/case-assignment/case.assignment.controller.test.ts b/backend/functions/lib/controllers/case-assignment/case.assignment.controller.test.ts index 6851f0686..d38f6c919 100644 --- a/backend/functions/lib/controllers/case-assignment/case.assignment.controller.test.ts +++ b/backend/functions/lib/controllers/case-assignment/case.assignment.controller.test.ts @@ -140,11 +140,18 @@ describe('Case Assignment Creation Tests', () => { }); test('should fetch a list of assignments when a GET request is called', async () => { - const assignments = MockData.buildArray(MockData.getAttorneyAssignment, 3); + const caseId = '111-22-33333'; + const assignments = MockData.buildArray(() => MockData.getAttorneyAssignment({ caseId }), 3); const camsHttpResponse = httpSuccess({ body: { data: assignments } }); + const expectedMap = new Map([[caseId, assignments]]); + applicationContext.request = mockCamsHttpRequest({ + method: 'GET', + params: { id: caseId }, + }); + jest .spyOn(CaseAssignmentUseCase.prototype, 'findAssignmentsByCaseId') - .mockResolvedValue(assignments); + .mockResolvedValue(expectedMap); const assignmentController = new CaseAssignmentController(applicationContext); const result = await assignmentController.handleRequest(applicationContext); diff --git a/backend/functions/lib/controllers/case-assignment/case.assignment.controller.ts b/backend/functions/lib/controllers/case-assignment/case.assignment.controller.ts index b62a48ea0..bd086063c 100644 --- a/backend/functions/lib/controllers/case-assignment/case.assignment.controller.ts +++ b/backend/functions/lib/controllers/case-assignment/case.assignment.controller.ts @@ -43,9 +43,10 @@ export class CaseAssignmentController implements CamsController { statusCode: HttpStatusCodes.CREATED, }); } else { - const assignments = await assignmentUseCase.findAssignmentsByCaseId( + const assignmentsMap = await assignmentUseCase.findAssignmentsByCaseId([ context.request.params['id'], - ); + ]); + const assignments = assignmentsMap.get(context.request.params['id']); return httpSuccess({ body: { data: assignments, diff --git a/backend/functions/lib/controllers/cases/cases.controller.test.ts b/backend/functions/lib/controllers/cases/cases.controller.test.ts index bbce4cd1c..1d5461859 100644 --- a/backend/functions/lib/controllers/cases/cases.controller.test.ts +++ b/backend/functions/lib/controllers/cases/cases.controller.test.ts @@ -289,29 +289,50 @@ describe('cases controller test', () => { expect(actual).toEqual(expected); }); - test('should properly search for a list of division codes', async () => { - const data = [MockData.getCaseBasics()]; - - const divisionCodeOne = 'hello'; - const divisionCodeTwo = 'world'; - - const expected = { - divisionCodes: [divisionCodeOne, divisionCodeTwo], - limit: 25, - offset: 0, - }; - - const useCaseSpy = jest - .spyOn(CaseManagement.prototype, 'searchCases') - .mockResolvedValue(data); - - const camsHttpRequest = mockCamsHttpRequest({ - method: 'POST', - body: expected, - }); - await controller.searchCases(camsHttpRequest); - expect(useCaseSpy).toHaveBeenCalledWith(expect.anything(), expected); - }); + const optionsCases = [ + { + caseName: 'SHOULD NOT search for case assignments WITH options', + options: { includeAssignments: 'false' }, + result: false, + }, + { + caseName: 'SHOULD NOT search for case assignments WITHOUT options', + options: undefined, + result: false, + }, + { + caseName: 'SHOULD search for case assignments', + options: { includeAssignments: 'true' }, + result: true, + }, + ]; + test.each(optionsCases)( + 'should properly search for a list of division codes and $caseName', + async (args) => { + const data = [MockData.getCaseBasics()]; + + const divisionCodeOne = 'hello'; + const divisionCodeTwo = 'world'; + + const expected = { + divisionCodes: [divisionCodeOne, divisionCodeTwo], + limit: 25, + offset: 0, + }; + + const useCaseSpy = jest + .spyOn(CaseManagement.prototype, 'searchCases') + .mockResolvedValue(data); + + const camsHttpRequest = mockCamsHttpRequest({ + method: 'POST', + body: expected, + query: args.options, + }); + await controller.searchCases(camsHttpRequest); + expect(useCaseSpy).toHaveBeenCalledWith(expect.anything(), expected, args.result); + }, + ); test('should return an error if an error is encountered', async () => { const caseNumber = '00-00000'; diff --git a/backend/functions/lib/controllers/cases/cases.controller.ts b/backend/functions/lib/controllers/cases/cases.controller.ts index abc9da126..57ea5dd72 100644 --- a/backend/functions/lib/controllers/cases/cases.controller.ts +++ b/backend/functions/lib/controllers/cases/cases.controller.ts @@ -17,6 +17,10 @@ function getCurrentPage(caseLength: number, predicate: CasesSearchPredicate) { return caseLength === 0 ? 0 : predicate.offset / predicate.limit + 1; } +type SearchOptions = { + includeAssignments?: string; +}; + export class CasesController implements CamsController { private readonly applicationContext: ApplicationContext; private readonly caseManagement: CaseManagement; @@ -52,21 +56,29 @@ export class CasesController implements CamsController { public async searchCases(request: CamsHttpRequest) { const predicate = request.body as CasesSearchPredicate; - const body = await this.paginateSearchCases(predicate, request.url); + const options = request.query as SearchOptions; + const includeAssignments = options?.includeAssignments === 'true'; + const body = await this.paginateSearchCases(predicate, request.url, !!includeAssignments); return body; } async paginateSearchCases( predicate: CasesSearchPredicate, url: string, + includeAssignments: boolean, ): Promise[]>> { - const cases = await this.caseManagement.searchCases(this.applicationContext, predicate); + const cases = await this.caseManagement.searchCases( + this.applicationContext, + predicate, + includeAssignments, + ); const pagination: Pagination = { count: cases.length, limit: predicate.limit, currentPage: getCurrentPage(cases.length, predicate), }; + if (cases.length > predicate.limit) { const next = new URL(url); next.searchParams.set('limit', predicate.limit.toString()); @@ -75,12 +87,14 @@ export class CasesController implements CamsController { cases.pop(); pagination.count = cases.length; } + if (predicate.offset > 0) { const previous = new URL(url); previous.searchParams.set('limit', predicate.limit.toString()); previous.searchParams.set('offset', (predicate.offset - predicate.limit).toString()); pagination.previous = previous.href; } + return { meta: { self: url, diff --git a/backend/functions/lib/use-cases/case-assignment.test.ts b/backend/functions/lib/use-cases/case-assignment.test.ts index 46aa9ca17..15ecdb4f6 100644 --- a/backend/functions/lib/use-cases/case-assignment.test.ts +++ b/backend/functions/lib/use-cases/case-assignment.test.ts @@ -98,14 +98,16 @@ describe('Case assignment tests', () => { assignedOn: new Date().toISOString(), }, ]; - findAssignmentsByCaseId.mockResolvedValue(assignments); + + const expectedMap = new Map([[caseId, assignments]]); + findAssignmentsByCaseId.mockResolvedValue(expectedMap); const assignmentUseCase = new CaseAssignmentUseCase(applicationContext); - const actualAssignments = await assignmentUseCase.findAssignmentsByCaseId(caseId); + const actualAssignments = await assignmentUseCase.findAssignmentsByCaseId([caseId]); - expect(actualAssignments.length).toEqual(2); - expect(actualAssignments).toEqual(expect.arrayContaining(assignments)); + expect(actualAssignments.get(caseId).length).toEqual(2); + expect(actualAssignments).toEqual(expectedMap); }); }); @@ -145,6 +147,8 @@ describe('Case assignment tests', () => { role, }; + findAssignmentsByCaseId.mockResolvedValue(new Map([[caseId, []]])); + expect(createAssignment.mock.calls[0][0]).toEqual(expect.objectContaining(assignmentOne)); expect(createAssignment.mock.calls[1][0]).toEqual(expect.objectContaining(assignmentTwo)); }); @@ -172,9 +176,7 @@ describe('Case assignment tests', () => { role, }; - jest - .spyOn(MockMongoRepository.prototype, 'findAssignmentsByCaseId') - .mockResolvedValue([assignmentOne]); + findAssignmentsByCaseId.mockResolvedValue(new Map([[caseId, [assignmentOne]]])); await assignmentUseCase.createTrialAttorneyAssignments( applicationContext, @@ -205,7 +207,7 @@ describe('Case assignment tests', () => { role, }; - findAssignmentsByCaseId.mockResolvedValue([assignmentOne]); + findAssignmentsByCaseId.mockResolvedValue(new Map([[caseId, [assignmentOne]]])); await assignmentUseCase.createTrialAttorneyAssignments( applicationContext, diff --git a/backend/functions/lib/use-cases/case-assignment.ts b/backend/functions/lib/use-cases/case-assignment.ts index 0523b3a8f..8c94b8683 100644 --- a/backend/functions/lib/use-cases/case-assignment.ts +++ b/backend/functions/lib/use-cases/case-assignment.ts @@ -85,8 +85,10 @@ export class CaseAssignmentUseCase { }); const listOfAssignmentIdsCreated: string[] = []; - const existingAssignmentRecords = - await this.assignmentRepository.findAssignmentsByCaseId(caseId); + const existingAssignmentRecordsMap = await this.assignmentRepository.findAssignmentsByCaseId([ + caseId, + ]); + const existingAssignmentRecords = existingAssignmentRecordsMap.get(caseId) ?? []; for (const existingAssignment of existingAssignmentRecords) { const stillAssigned = listOfAssignments.find((newAssignment) => { return ( @@ -113,7 +115,10 @@ export class CaseAssignmentUseCase { } } - const newAssignmentRecords = await this.assignmentRepository.findAssignmentsByCaseId(caseId); + const newAssignmentRecordsMap = await this.assignmentRepository.findAssignmentsByCaseId([ + caseId, + ]); + const newAssignmentRecords = newAssignmentRecordsMap.get(caseId); const history = createAuditRecord( { caseId, @@ -135,8 +140,8 @@ export class CaseAssignmentUseCase { return listOfAssignmentIdsCreated; } - public async findAssignmentsByCaseId(caseId: string): Promise { - return await this.assignmentRepository.findAssignmentsByCaseId(caseId); + public async findAssignmentsByCaseId(caseIds: string[]): Promise> { + return await this.assignmentRepository.findAssignmentsByCaseId(caseIds); } public async getCaseLoad(userId: string): Promise { diff --git a/backend/functions/lib/use-cases/case-management.test.ts b/backend/functions/lib/use-cases/case-management.test.ts index c593471cb..4c41eee15 100644 --- a/backend/functions/lib/use-cases/case-management.test.ts +++ b/backend/functions/lib/use-cases/case-management.test.ts @@ -50,15 +50,17 @@ const assignments: CaseAssignment[] = [ ]; const caseIdWithAssignments = '081-23-01176'; +const assignmentMap = new Map([[caseIdWithAssignments, assignments]]); + jest.mock('./case-assignment', () => { return { CaseAssignmentUseCase: jest.fn().mockImplementation(() => { return { - findAssignmentsByCaseId: (caseId: string) => { - if (caseId === 'ThrowError') { + findAssignmentsByCaseId: (caseIds: string[]) => { + if (caseIds[0] === 'ThrowError') { throw new Error('TestError'); - } else if (caseId === caseIdWithAssignments) { - return Promise.resolve(assignments); + } else if (caseIds[0] === caseIdWithAssignments) { + return Promise.resolve(assignmentMap); } else { return Promise.resolve([]); } @@ -200,10 +202,10 @@ describe('Case management tests', () => { const caseId = caseIdWithAssignments; const dateFiled = '2018-11-16'; const closedDate = '2019-06-21'; - const assignments = [attorneyJaneSmith, attorneyJoeNobel]; + const caseDetail = MockData.getCaseDetail({ override: { - caseId: caseId, + caseId, dateFiled, closedDate, }, @@ -214,6 +216,8 @@ describe('Case management tests', () => { return Promise.resolve(caseDetail); }); + jest.spyOn(chapterCaseList.casesGateway, 'getCaseDetail').mockResolvedValue(caseDetail); + const actualCaseDetail = await chapterCaseList.getCaseDetail(applicationContext, caseId); expect(actualCaseDetail.caseId).toEqual(caseId); @@ -226,11 +230,11 @@ describe('Case management tests', () => { const officeName = 'Test Office'; const courtOffices = ustpOfficeToCourtDivision(applicationContext.session.user.offices[0]); const officeDetail = courtOffices[0]; - const caseNumber = '00-00000'; + const bCase = MockData.getCaseDetail({ override: { ...officeDetail, - caseId: '999-' + caseNumber, + caseId: caseIdWithAssignments, }, }); @@ -246,6 +250,7 @@ describe('Case management tests', () => { const expected = { ...bCase, + assignments, officeName, _actions, officeCode: builtOfficeCode, @@ -284,15 +289,44 @@ describe('Case management tests', () => { test('should return an empty array for no matches', async () => { jest.spyOn(useCase.casesGateway, 'searchCases').mockResolvedValue([]); - const actual = await useCase.searchCases(applicationContext, { caseNumber }); + const actual = await useCase.searchCases(applicationContext, { caseNumber }, false); expect(actual).toEqual([]); }); - test('should return a match', async () => { + const optionsCases = [ + { caseName: 'NOT get case assignments', includeCaseAssignments: false }, + { caseName: 'GET case assignments', includeCaseAssignments: true }, + ]; + test.each(optionsCases)(`should return a match and $caseName`, async (args) => { const caseList = [MockData.getCaseSummary({ override: { caseId: '999-' + caseNumber } })]; jest.spyOn(useCase.casesGateway, 'searchCases').mockResolvedValue(caseList); - const actual = await useCase.searchCases(applicationContext, { caseNumber }); + const assignmentsSpy = jest + .spyOn(MockMongoRepository.prototype, 'findAssignmentsByCaseId') + .mockImplementation(() => { + if (args.includeCaseAssignments) { + return Promise.resolve( + new Map([ + [ + caseList[0].caseId, + [MockData.getAttorneyAssignment({ caseId: caseList[0].caseId })], + ], + ]), + ); + } else { + return Promise.reject('We should not have retrieved assignments.'); + } + }); + const actual = await useCase.searchCases( + applicationContext, + { caseNumber }, + args.includeCaseAssignments, + ); expect(actual).toEqual(caseList); + if (args.includeCaseAssignments) { + expect(assignmentsSpy).toHaveBeenCalled(); + } else { + expect(assignmentsSpy).not.toHaveBeenCalled(); + } }); test('should return cases and actions for the user', async () => { @@ -315,7 +349,7 @@ describe('Case management tests', () => { const expected = [{ ...bCase, officeCode, _actions }]; jest.spyOn(useCase.casesGateway, 'searchCases').mockResolvedValue([bCase]); - const actual = await useCase.searchCases(applicationContext, { caseNumber }); + const actual = await useCase.searchCases(applicationContext, { caseNumber }, false); expect(actual).toEqual(expected); }); @@ -331,7 +365,7 @@ describe('Case management tests', () => { .mockResolvedValue(assignments); const searchCases = jest.spyOn(useCase.casesGateway, 'searchCases').mockResolvedValue(cases); - const actual = await useCase.searchCases(applicationContext, { assignments: [user] }); + const actual = await useCase.searchCases(applicationContext, { assignments: [user] }, false); expect(actual).toEqual(cases); expect(findAssignmentsByAssignee).toHaveBeenCalledWith(user.id); @@ -349,7 +383,7 @@ describe('Case management tests', () => { originalError: error, }); jest.spyOn(useCase.casesGateway, 'searchCases').mockRejectedValue(error); - await expect(useCase.searchCases(applicationContext, { caseNumber })).rejects.toThrow( + await expect(useCase.searchCases(applicationContext, { caseNumber }, false)).rejects.toThrow( expectedError, ); }); @@ -357,7 +391,9 @@ describe('Case management tests', () => { test('should throw CamsError', async () => { const error = new CamsError('TEST', { message: 'test error' }); jest.spyOn(useCase.casesGateway, 'searchCases').mockRejectedValue(error); - await expect(useCase.searchCases(applicationContext, { caseNumber })).rejects.toThrow(error); + await expect(useCase.searchCases(applicationContext, { caseNumber }, false)).rejects.toThrow( + error, + ); }); }); }); diff --git a/backend/functions/lib/use-cases/case-management.ts b/backend/functions/lib/use-cases/case-management.ts index 49e616010..da7952811 100644 --- a/backend/functions/lib/use-cases/case-management.ts +++ b/backend/functions/lib/use-cases/case-management.ts @@ -13,12 +13,12 @@ import { isCamsError } from '../common-errors/cams-error'; import { AssignmentError } from './assignment.exception'; import { OfficesGateway } from './offices/offices.types'; import { CaseAssignmentRepository, CasesRepository } from './gateways.types'; -import { CaseAssignment } from '../../../../common/src/cams/assignments'; import { CasesSearchPredicate } from '../../../../common/src/api/search'; import Actions, { Action, ResourceActions } from '../../../../common/src/cams/actions'; import { CamsRole } from '../../../../common/src/cams/roles'; -import { CamsUserReference, getCourtDivisionCodes } from '../../../../common/src/cams/users'; +import { getCourtDivisionCodes } from '../../../../common/src/cams/users'; import { buildOfficeCode } from './offices/offices'; +import { CaseAssignment } from '../../../../common/src/cams/assignments'; const MODULE_NAME = 'CASE-MANAGEMENT-USE-CASE'; @@ -57,6 +57,7 @@ export default class CaseManagement { public async searchCases( context: ApplicationContext, predicate: CasesSearchPredicate, + includeAssignments: boolean, ): Promise[]> { try { if (predicate.assignments && predicate.assignments.length > 0) { @@ -69,19 +70,36 @@ export default class CaseManagement { } predicate.caseIds = Array.from(caseIdSet); // if we're requesting cases with specific assignments, and none are found, return [] early + if (predicate.caseIds.length == 0) { return []; } } + const cases: ResourceActions[] = await this.casesGateway.searchCases( context, predicate, ); + + const caseIds = []; for (const casesKey in cases) { + caseIds.push(cases[casesKey].caseId); const bCase = cases[casesKey]; bCase.officeCode = buildOfficeCode(bCase.regionId, bCase.courtDivisionCode); bCase._actions = getAction(context, bCase); } + + if (includeAssignments) { + const assignmentsMap = await this.assignmentGateway.findAssignmentsByCaseId(caseIds); + for (const casesKey in cases) { + const assignments = assignmentsMap.get(cases[casesKey].caseId) ?? []; + cases[casesKey] = { + ...cases[casesKey], + assignments, + }; + } + } + return cases; } catch (originalError) { if (!isCamsError(originalError)) { @@ -123,15 +141,11 @@ export default class CaseManagement { private async getCaseAssignments( context: ApplicationContext, bCase: CaseDetail, - ): Promise { + ): Promise { const caseAssignment = new CaseAssignmentUseCase(context); try { - const assignments: CaseAssignment[] = await caseAssignment.findAssignmentsByCaseId( - bCase.caseId, - ); - return assignments.map((a) => { - return { id: a.userId, name: a.name }; - }); + const assignmentsMap = await caseAssignment.findAssignmentsByCaseId([bCase.caseId]); + return assignmentsMap.get(bCase.caseId); } catch (e) { throw new AssignmentError(MODULE_NAME, { message: diff --git a/backend/functions/lib/use-cases/gateways.types.ts b/backend/functions/lib/use-cases/gateways.types.ts index c9b7b7522..3ac1f0d87 100644 --- a/backend/functions/lib/use-cases/gateways.types.ts +++ b/backend/functions/lib/use-cases/gateways.types.ts @@ -60,7 +60,7 @@ export interface UserSessionCacheRepository extends Reads, U export interface CaseAssignmentRepository extends Creates, Updates { - findAssignmentsByCaseId(caseId: string): Promise; + findAssignmentsByCaseId(caseIds: string[]): Promise>; findAssignmentsByAssignee(userId: string): Promise; } diff --git a/backend/functions/lib/use-cases/offices/offices.ts b/backend/functions/lib/use-cases/offices/offices.ts index d977e1b2b..7aafbe4b8 100644 --- a/backend/functions/lib/use-cases/offices/offices.ts +++ b/backend/functions/lib/use-cases/offices/offices.ts @@ -1,5 +1,5 @@ import { UstpOfficeDetails } from '../../../../../common/src/cams/offices'; -import { AttorneyUser, CamsUserReference } from '../../../../../common/src/cams/users'; +import { AttorneyUser, Staff } from '../../../../../common/src/cams/users'; import { ApplicationContext } from '../../adapters/types/basic'; import { getOfficesGateway, @@ -57,7 +57,7 @@ export class OfficesUseCase { const roleGroups = userGroups.filter((group) => groupToRoleMap.has(group.name)); // Map roles to users. - const userMap = new Map(); + const userMap = new Map(); for (const roleGroup of roleGroups) { const users = await userGroupSource.getUserGroupUsers(context, config, roleGroup); const role = groupToRoleMap.get(roleGroup.name); @@ -65,8 +65,7 @@ export class OfficesUseCase { if (userMap.has(user.id)) { userMap.get(user.id).roles.push(role); } else { - user.roles = [role]; - userMap.set(user.id, user); + userMap.set(user.id, { ...user, roles: [role] }); } } } diff --git a/backend/functions/lib/use-cases/orders/orders-consolidation-approval.test.ts b/backend/functions/lib/use-cases/orders/orders-consolidation-approval.test.ts index 6f1840ae9..d0a487302 100644 --- a/backend/functions/lib/use-cases/orders/orders-consolidation-approval.test.ts +++ b/backend/functions/lib/use-cases/orders/orders-consolidation-approval.test.ts @@ -150,7 +150,19 @@ describe('Orders use case', () => { return []; }); - jest.spyOn(MockMongoRepository.prototype, 'findAssignmentsByCaseId').mockResolvedValue([]); + jest.spyOn(MockMongoRepository.prototype, 'update').mockResolvedValue(undefined); + + jest.spyOn(MockMongoRepository.prototype, 'create').mockResolvedValue(undefined); + + jest + .spyOn(MockMongoRepository.prototype, 'findAssignmentsByCaseId') + .mockImplementation((ids: string[]) => { + const assignmentsMap = new Map(); + ids.forEach((id) => { + assignmentsMap.set(id, [MockData.getAttorneyAssignment({ id })]); + }); + return Promise.resolve(assignmentsMap); + }); jest .spyOn(MockMongoRepository.prototype, 'createConsolidationTo') .mockResolvedValue(MockData.getConsolidationTo({ override: { otherCase: leadCaseSummary } })); @@ -174,8 +186,8 @@ describe('Orders use case', () => { caseId: originalConsolidation.childCases[0].caseId, documentType: 'AUDIT_ASSIGNMENT', updatedBy: expect.anything(), - before: [], - after: [], + before: expect.anything(), + after: expect.anything(), }), ); expect(mockCreateHistory.mock.calls[2][0]).toEqual(expect.objectContaining(leadCaseHistory)); @@ -276,7 +288,9 @@ describe('Orders use case', () => { const mockGetConsolidation = jest.spyOn(casesRepo, 'getConsolidation').mockResolvedValue([]); - jest.spyOn(MockMongoRepository.prototype, 'findAssignmentsByCaseId').mockResolvedValue([]); + jest + .spyOn(MockMongoRepository.prototype, 'findAssignmentsByCaseId') + .mockResolvedValue(new Map()); jest .spyOn(MockMongoRepository.prototype, 'createConsolidationTo') .mockResolvedValue(MockData.getConsolidationTo()); diff --git a/backend/functions/lib/use-cases/orders/orders.test.ts b/backend/functions/lib/use-cases/orders/orders.test.ts index 7192a2c98..6841eb5c0 100644 --- a/backend/functions/lib/use-cases/orders/orders.test.ts +++ b/backend/functions/lib/use-cases/orders/orders.test.ts @@ -579,9 +579,13 @@ describe('Orders use case', () => { ); }); + const caseId = '111-22-33333'; + const assignments = MockData.buildArray(() => MockData.getAttorneyAssignment({ caseId }), 3); + const expectedMap = new Map([[caseId, assignments]]); + jest .spyOn(MockMongoRepository.prototype, 'findAssignmentsByCaseId') - .mockResolvedValue(MockData.buildArray(MockData.getAttorneyAssignment, 3)); + .mockResolvedValue(expectedMap); jest .spyOn(MockMongoRepository.prototype, 'createConsolidationTo') diff --git a/backend/functions/lib/use-cases/orders/orders.ts b/backend/functions/lib/use-cases/orders/orders.ts index a4c901fe7..d07d429bf 100644 --- a/backend/functions/lib/use-cases/orders/orders.ts +++ b/backend/functions/lib/use-cases/orders/orders.ts @@ -395,7 +395,10 @@ export class OrdersUseCase { if (status === 'approved') { const assignmentUseCase = new CaseAssignmentUseCase(context); - const leadCaseAssignments = await assignmentUseCase.findAssignmentsByCaseId(leadCase.caseId); + const leadCaseAssignmentsMap = await assignmentUseCase.findAssignmentsByCaseId([ + leadCase.caseId, + ]); + const leadCaseAssignments = leadCaseAssignmentsMap.get(leadCase.caseId) ?? []; const leadCaseAttorneys: CamsUserReference[] = leadCaseAssignments.map((assignment) => { return { id: assignment.caseId, name: assignment.name }; }); diff --git a/common/src/cams/cases.ts b/common/src/cams/cases.ts index 0e5606fcb..4ea554dbb 100644 --- a/common/src/cams/cases.ts +++ b/common/src/cams/cases.ts @@ -1,8 +1,8 @@ import { DebtorAttorney, Party } from './parties'; import { ConsolidationFrom, ConsolidationTo, TransferFrom, TransferTo } from './events'; -import { AttorneyUser, CamsUserReference } from './users'; +import { CaseAssignment } from './assignments'; -export interface LegacyOfficeDetails { +export type FlatOfficeDetail = { officeName: string; officeCode: string; courtId: string; @@ -13,13 +13,9 @@ export interface LegacyOfficeDetails { regionId: string; regionName: string; state?: string; - staff?: CamsUserReference[]; -} +}; -// TODO: Decouple the case model from office details. -// Why do we couple to office details anyhow? It's because we flatten office details -// into case detail in DXTR SQL JOINs. -export interface CaseBasics extends LegacyOfficeDetails { +export type CaseBasics = FlatOfficeDetail & { dxtrId: string; // TODO: Refactor this out so it doesn't leak to the UI. caseId: string; chapter: string; @@ -29,24 +25,23 @@ export interface CaseBasics extends LegacyOfficeDetails { petitionLabel?: string; debtorTypeCode?: string; debtorTypeLabel?: string; - assignments?: AttorneyUser[]; -} + assignments?: CaseAssignment[]; +}; -export interface CaseSummary extends CaseBasics { +export type CaseSummary = CaseBasics & { debtor: Party; -} +}; -export interface CaseDetail extends CaseSummary { +export type CaseDetail = CaseSummary & { closedDate?: string; dismissedDate?: string; reopenedDate?: string; courtId: string; - assignments?: AttorneyUser[]; transfers?: Array; consolidation?: Array; debtorAttorney?: DebtorAttorney; judgeName?: string; -} +}; export type CaseDocketEntryDocument = { fileUri: string; diff --git a/common/src/cams/offices.ts b/common/src/cams/offices.ts index 378995e2c..176f53523 100644 --- a/common/src/cams/offices.ts +++ b/common/src/cams/offices.ts @@ -1,4 +1,4 @@ -import { CamsUserReference } from './users'; +import { Staff } from './users'; //TODO: Some of these probably do not belong here export type UstpOfficeDetails = { @@ -9,7 +9,7 @@ export type UstpOfficeDetails = { regionId: string; // DXTR AO_REGION regionName: string; // DXTR AO_REGION state?: string; // https://www.justice.gov/ust/us-trustee-regions-and-offices - staff?: CamsUserReference[]; + staff?: Staff[]; }; export type UstpGroup = { diff --git a/common/src/cams/session.test.ts b/common/src/cams/session.test.ts index 39c06f999..150b0950a 100644 --- a/common/src/cams/session.test.ts +++ b/common/src/cams/session.test.ts @@ -13,9 +13,8 @@ describe('session', () => { describe('getCamsUserReference', () => { test('should return a CamsUserReference with expected properties', () => { - const roles = [CamsRole.CaseAssignmentManager]; - const user = MockData.getCamsUser({ roles }); - const expected = { id: user.id, name: user.name, roles }; + const user = MockData.getCamsUser(); + const expected = { id: user.id, name: user.name }; const actual = getCamsUserReference(user); expect(actual).toEqual(expected); }); diff --git a/common/src/cams/session.ts b/common/src/cams/session.ts index 3cb8ae696..79936342a 100644 --- a/common/src/cams/session.ts +++ b/common/src/cams/session.ts @@ -9,6 +9,6 @@ export type CamsSession = { }; export function getCamsUserReference(user: T): CamsUserReference { - const { id, name, roles } = user; - return { id, name, roles }; + const { id, name } = user; + return { id, name }; } diff --git a/common/src/cams/test-utilities/mock-data.ts b/common/src/cams/test-utilities/mock-data.ts index ed8c4598b..df6517221 100644 --- a/common/src/cams/test-utilities/mock-data.ts +++ b/common/src/cams/test-utilities/mock-data.ts @@ -499,7 +499,7 @@ function getCamsUser(override: Partial = {}): CamsUser { function getAttorneyUser(override: Partial = {}): AttorneyUser { return { - ...getCamsUser(), + ...getCamsUser({ roles: [CamsRole.TrialAttorney] }), ...override, }; } diff --git a/common/src/cams/users.ts b/common/src/cams/users.ts index d8f69164d..ff58554dd 100644 --- a/common/src/cams/users.ts +++ b/common/src/cams/users.ts @@ -4,11 +4,15 @@ import { CamsRole } from './roles'; export type CamsUserReference = { id: string; name: string; +}; + +export type Staff = CamsUserReference & { roles?: CamsRole[]; }; export type CamsUser = CamsUserReference & { offices?: UstpOfficeDetails[]; + roles?: CamsRole[]; }; export type AttorneyUser = CamsUser & { diff --git a/ops/cloud-deployment/lib/cosmos/mongo/cosmos-account.bicep b/ops/cloud-deployment/lib/cosmos/mongo/cosmos-account.bicep index e70e37958..87430ee3f 100644 --- a/ops/cloud-deployment/lib/cosmos/mongo/cosmos-account.bicep +++ b/ops/cloud-deployment/lib/cosmos/mongo/cosmos-account.bicep @@ -135,6 +135,9 @@ resource account 'Microsoft.DocumentDB/databaseAccounts@2023-04-15' = { { name: 'EnableMongoRoleBasedAccessControl' } + { + name: 'EnableUniqueCompoundNestedDocs' + } ] publicNetworkAccess: 'Enabled' isVirtualNetworkFilterEnabled: allowAllNetworks ? false : true diff --git a/ops/cloud-deployment/lib/cosmos/mongo/cosmos-collections.bicep b/ops/cloud-deployment/lib/cosmos/mongo/cosmos-collections.bicep index 4118e5e8d..f726b997d 100644 --- a/ops/cloud-deployment/lib/cosmos/mongo/cosmos-collections.bicep +++ b/ops/cloud-deployment/lib/cosmos/mongo/cosmos-collections.bicep @@ -119,10 +119,11 @@ resource officesCollection 'Microsoft.DocumentDB/databaseAccounts/mongodbDatabas key: { keys: [ 'officeCode' + 'userId' ] } options: { - unique: true + unique:true } } ] diff --git a/test/e2e/playwright/auth-setup.ts b/test/e2e/playwright/auth-setup.ts index 3d71c7f8d..b6b491aa7 100644 --- a/test/e2e/playwright/auth-setup.ts +++ b/test/e2e/playwright/auth-setup.ts @@ -1,5 +1,5 @@ -import { test as setup } from '@playwright/test'; import { Page, expect } from '@playwright/test'; +import { test } from './fixture/urlQueryString'; /* eslint-disable-next-line @typescript-eslint/no-require-imports */ require('dotenv').config(); @@ -10,7 +10,7 @@ const TARGET_HOST = process.env.TARGET_HOST; const LOGIN_PATH = '/login'; const timeoutOption = { timeout: 30000 }; -setup('authenticate', async ({ page }) => { +test('authenticate', async ({ page }) => { const { login } = usingAuthenticationProvider(); await login(page); }); diff --git a/user-interface/src/case-detail/CaseDetailScreen.test.tsx b/user-interface/src/case-detail/CaseDetailScreen.test.tsx index 43700b6fa..212d88c04 100644 --- a/user-interface/src/case-detail/CaseDetailScreen.test.tsx +++ b/user-interface/src/case-detail/CaseDetailScreen.test.tsx @@ -8,11 +8,14 @@ import { CaseDetail } from '@common/cams/cases'; import { Debtor, DebtorAttorney } from '@common/cams/parties'; import { MockAttorneys } from '@common/cams/test-utilities/attorneys.mock'; import * as detailHeader from './panels/CaseDetailHeader'; +import MockData from '@common/cams/test-utilities/mock-data'; const caseId = '101-23-12345'; const brianWilson = MockAttorneys.Brian; +const brianAssignment = MockData.getAttorneyAssignment({ ...brianWilson }); const carlWilson = MockAttorneys.Carl; +const carlAssignment = MockData.getAttorneyAssignment({ ...carlWilson }); const rickBHartName = 'Rick B Hart'; @@ -59,7 +62,7 @@ describe('Case Detail screen tests', () => { petitionLabel: 'Voluntary', closedDate: '01-08-1963', dismissedDate: '01-08-1964', - assignments: [brianWilson, carlWilson], + assignments: [brianAssignment, carlAssignment], debtor: { name: 'Roger Rabbit', address1: '123 Rabbithole Lane', @@ -102,7 +105,7 @@ describe('Case Detail screen tests', () => { petitionLabel: 'Voluntary', closedDate: '01-08-1963', dismissedDate: '01-08-1964', - assignments: [brianWilson, carlWilson], + assignments: [brianAssignment, carlAssignment], debtor: { name: 'Roger Rabbit', address1: '123 Rabbithole Lane', @@ -214,7 +217,7 @@ describe('Case Detail screen tests', () => { petitionLabel: 'Voluntary', closedDate: '01-08-1963', dismissedDate: '01-08-1964', - assignments: [brianWilson, carlWilson], + assignments: [brianAssignment, carlAssignment], debtor: { name: 'Roger Rabbit', }, @@ -274,7 +277,7 @@ describe('Case Detail screen tests', () => { petitionLabel: 'Voluntary', closedDate: '01-08-1963', dismissedDate: '01-08-1964', - assignments: [brianWilson, carlWilson], + assignments: [brianAssignment, carlAssignment], debtor: { name: 'Roger Rabbit', address1, @@ -345,7 +348,7 @@ describe('Case Detail screen tests', () => { petitionLabel: 'Voluntary', closedDate: '01-08-1963', dismissedDate: '01-08-1964', - assignments: [brianWilson, carlWilson], + assignments: [brianAssignment, carlAssignment], debtor: { name: 'Roger Rabbit', ssn, @@ -406,7 +409,7 @@ describe('Case Detail screen tests', () => { debtorTypeLabel: 'Corporate Business', petitionLabel: 'Voluntary', dismissedDate: '01-08-1964', - assignments: [brianWilson, carlWilson], + assignments: [brianAssignment, carlAssignment], debtor: { name: 'Roger Rabbit', }, @@ -445,7 +448,7 @@ describe('Case Detail screen tests', () => { dateFiled: '01-04-1962', closedDate: '01-08-1963', dismissedDate: '01-08-1964', - assignments: [brianWilson, carlWilson], + assignments: [brianAssignment, carlAssignment], judgeName: 'Honorable Jason Smith', debtorTypeLabel: 'Corporate Business', petitionLabel: 'Voluntary', @@ -489,7 +492,7 @@ describe('Case Detail screen tests', () => { debtorTypeLabel: 'Corporate Business', petitionLabel: 'Voluntary', closedDate: '01-08-1963', - assignments: [brianWilson, carlWilson], + assignments: [brianAssignment, carlAssignment], debtor: { name: 'Roger Rabbit', }, @@ -532,7 +535,7 @@ describe('Case Detail screen tests', () => { petitionLabel: 'Voluntary', closedDate: '01-08-1963', reopenedDate: '04-15-1969', - assignments: [brianWilson, carlWilson], + assignments: [brianAssignment, carlAssignment], debtor: { name: 'Roger Rabbit', }, @@ -581,7 +584,7 @@ describe('Case Detail screen tests', () => { petitionLabel: 'Voluntary', reopenedDate: '04-15-1969', closedDate: '08-08-1970', - assignments: [brianWilson, carlWilson], + assignments: [brianAssignment, carlAssignment], debtor: { name: 'Roger Rabbit', }, @@ -712,7 +715,7 @@ describe('Case Detail screen tests', () => { dateFiled: '01-04-1962', closedDate: '01-08-1963', dismissedDate: '01-08-1964', - assignments: [brianWilson, carlWilson], + assignments: [brianAssignment, carlAssignment], debtor: { name: 'Roger Rabbit', }, diff --git a/user-interface/src/case-detail/CaseDetailScreen.tsx b/user-interface/src/case-detail/CaseDetailScreen.tsx index 287112c10..8e885b97c 100644 --- a/user-interface/src/case-detail/CaseDetailScreen.tsx +++ b/user-interface/src/case-detail/CaseDetailScreen.tsx @@ -24,6 +24,8 @@ import { useGlobalAlert } from '@/lib/hooks/UseGlobalAlert'; import DocumentTitle from '@/lib/components/cams/DocumentTitle/DocumentTitle'; import { MainContent } from '@/lib/components/cams/MainContent/MainContent'; import { useApi2 } from '@/lib/hooks/UseApi2'; +import { CaseAssignment } from '@common/cams/assignments'; +import { CamsRole } from '@common/cams/roles'; const CaseDetailHeader = lazy(() => import('./panels/CaseDetailHeader')); const CaseDetailBasicInfo = lazy(() => import('./panels/CaseDetailOverview')); @@ -317,9 +319,17 @@ export default function CaseDetailScreen(props: CaseDetailProps) { } function handleCaseAssignment(assignment: CallbackProps) { + const assignments: CaseAssignment[] = []; + assignment.selectedAttorneyList.forEach((attorney) => { + assignments.push({ + userId: attorney.id, + name: attorney.name, + role: CamsRole.TrialAttorney, + } as CaseAssignment); + }); const updatedCaseBasicInfo: CaseDetail = { ...caseBasicInfo!, - assignments: assignment.selectedAttorneyList, + assignments, }; setCaseBasicInfo(updatedCaseBasicInfo); } diff --git a/user-interface/src/case-detail/panels/CaseDetailOverview.test.tsx b/user-interface/src/case-detail/panels/CaseDetailOverview.test.tsx index 321076de8..b5e7f59b2 100644 --- a/user-interface/src/case-detail/panels/CaseDetailOverview.test.tsx +++ b/user-interface/src/case-detail/panels/CaseDetailOverview.test.tsx @@ -19,7 +19,9 @@ const TEST_CASE_ID = '101-23-12345'; const OLD_CASE_ID = '111-20-11111'; const NEW_CASE_ID = '222-24-00001'; const TEST_TRIAL_ATTORNEY_1 = MockAttorneys.Brian; +const TEST_ASSIGNMENT_1 = MockData.getAttorneyAssignment({ ...TEST_TRIAL_ATTORNEY_1 }); const TEST_TRIAL_ATTORNEY_2 = MockAttorneys.Carl; +const TEST_ASSIGNMENT_2 = MockData.getAttorneyAssignment({ ...TEST_TRIAL_ATTORNEY_2 }); const TEST_JUDGE_NAME = 'Rick B Hart'; const TEST_DEBTOR_ATTORNEY = MockData.getDebtorAttorney(); const BASE_TEST_CASE_DETAIL = MockData.getCaseDetail({ @@ -27,7 +29,7 @@ const BASE_TEST_CASE_DETAIL = MockData.getCaseDetail({ caseId: TEST_CASE_ID, chapter: '15', judgeName: TEST_JUDGE_NAME, - assignments: [TEST_TRIAL_ATTORNEY_1, TEST_TRIAL_ATTORNEY_2], + assignments: [TEST_ASSIGNMENT_1, TEST_ASSIGNMENT_2], debtorAttorney: TEST_DEBTOR_ATTORNEY, _actions: [Actions.ManageAssignments], }, @@ -118,7 +120,7 @@ describe('Case detail basic information panel', () => { caseId: TEST_CASE_ID, chapter: '15', judgeName: TEST_JUDGE_NAME, - assignments: [TEST_TRIAL_ATTORNEY_1, TEST_TRIAL_ATTORNEY_2], + assignments: [TEST_ASSIGNMENT_1, TEST_ASSIGNMENT_2], debtorAttorney: TEST_DEBTOR_ATTORNEY, }, }); diff --git a/user-interface/src/lib/models/api2.ts b/user-interface/src/lib/models/api2.ts index 0748addf7..47fc06ab5 100644 --- a/user-interface/src/lib/models/api2.ts +++ b/user-interface/src/lib/models/api2.ts @@ -73,6 +73,7 @@ interface GenericApiClient { body: U, options?: ObjectKeyVal, ): Promise | void>; + put( path: string, body: U, @@ -125,6 +126,7 @@ export function useGenericApi(): GenericApiClient { const body = await api.get(uriOrPathSubstring, options); return body as ResponseBody; }, + async patch( path: string, body: U, @@ -138,6 +140,7 @@ export function useGenericApi(): GenericApiClient { } return responseBody as ResponseBody; }, + async post( path: string, body: U, @@ -151,6 +154,7 @@ export function useGenericApi(): GenericApiClient { } return responseBody as ResponseBody; }, + async put( path: string, body: U, @@ -267,8 +271,11 @@ async function putConsolidationOrderRejection(data: ConsolidationOrderActionReje ); } -async function searchCases(predicate: CasesSearchPredicate) { - return api().post('/cases', predicate); +async function searchCases( + predicate: CasesSearchPredicate, + options: { includeAssignments?: boolean } = {}, +) { + return api().post('/cases', predicate, options); } async function postStaffAssignments(action: StaffAssignmentAction): Promise { diff --git a/user-interface/src/lib/testing/mock-api2.ts b/user-interface/src/lib/testing/mock-api2.ts index 4f6fb2c7c..903d8077d 100644 --- a/user-interface/src/lib/testing/mock-api2.ts +++ b/user-interface/src/lib/testing/mock-api2.ts @@ -275,8 +275,11 @@ async function putConsolidationOrderRejection( return put('/consolidations/reject', data); } -async function searchCases(predicate: CasesSearchPredicate): Promise> { - return post('/cases', predicate, {}); +async function searchCases( + predicate: CasesSearchPredicate, + options: { includeAssignments?: boolean } = {}, +): Promise> { + return post('/cases', predicate, options); } async function postStaffAssignments(action: StaffAssignmentAction): Promise { diff --git a/user-interface/src/search-results/SearchResults.tsx b/user-interface/src/search-results/SearchResults.tsx index 15005bada..66be68647 100644 --- a/user-interface/src/search-results/SearchResults.tsx +++ b/user-interface/src/search-results/SearchResults.tsx @@ -103,7 +103,7 @@ export function SearchResults(props: SearchResultsProps) { setIsSearching(true); if (onStartSearching) onStartSearching(); api - .searchCases(searchPredicate) + .searchCases(searchPredicate, { includeAssignments: true }) .then(handleSearchResults) .catch(handleSearchError) .finally(() => { diff --git a/user-interface/src/search/SearchScreen.test.tsx b/user-interface/src/search/SearchScreen.test.tsx index 8e8cf1fc5..9d475fe7c 100644 --- a/user-interface/src/search/SearchScreen.test.tsx +++ b/user-interface/src/search/SearchScreen.test.tsx @@ -29,7 +29,7 @@ describe('search screen', () => { }, data: [], }; - + const includeAssignments = { includeAssignments: true }; let searchCasesSpy: MockInstance; beforeEach(async () => { @@ -92,6 +92,7 @@ describe('search screen', () => { expect(rows).toHaveLength(caseList.length); expect(searchCasesSpy).toHaveBeenLastCalledWith( expect.objectContaining(divisionSearchPredicate), + includeAssignments, ); await testingUtilities.selectComboBoxItem('case-chapter-search', 3); @@ -113,6 +114,7 @@ describe('search screen', () => { expect(searchCasesSpy).toHaveBeenLastCalledWith( expect.objectContaining(divisionSearchPredicate), + includeAssignments, ); const pillButton = document.querySelector('#case-chapter-search .pill-clear-button'); @@ -192,6 +194,7 @@ describe('search screen', () => { expect(searchCasesSpy).toHaveBeenLastCalledWith( expect.objectContaining(divisionSearchPredicate), + includeAssignments, ); // Make second search request... @@ -217,6 +220,7 @@ describe('search screen', () => { expect(searchCasesSpy).toHaveBeenLastCalledWith( expect.objectContaining(divisionSearchPredicate), + includeAssignments, ); // clear division selection @@ -286,7 +290,7 @@ describe('search screen', () => { expect(table).not.toBeInTheDocument(); }); - expect(searchCasesSpy).toHaveBeenCalledWith(casesSearchPredicate); + expect(searchCasesSpy).toHaveBeenCalledWith(casesSearchPredicate, includeAssignments); }); test('should only search for full case number', async () => { diff --git a/user-interface/src/staff-assignment/modal/AssignAttorneyModal.test.tsx b/user-interface/src/staff-assignment/modal/AssignAttorneyModal.test.tsx index 44f992547..82ad6c7e2 100644 --- a/user-interface/src/staff-assignment/modal/AssignAttorneyModal.test.tsx +++ b/user-interface/src/staff-assignment/modal/AssignAttorneyModal.test.tsx @@ -169,15 +169,15 @@ describe('Test Assign Attorney Modal Component', () => { if (a.name > b.name) return 1; return 0; }) - .slice(1, 3) + .slice(1, 4) .map((attorney) => { - return { id: attorney.id, name: attorney.name, roles: [] }; + return { id: attorney.id, name: attorney.name }; }); await waitFor(() => { expect(postSpy).toHaveBeenCalledWith( expect.objectContaining({ - attorneyList: expect.arrayContaining(expectedAttorneys), + attorneyList: expectedAttorneys, caseId: '123', role: 'TrialAttorney', }), diff --git a/user-interface/src/staff-assignment/modal/AssignAttorneyModal.tsx b/user-interface/src/staff-assignment/modal/AssignAttorneyModal.tsx index adc0e8503..708783a2c 100644 --- a/user-interface/src/staff-assignment/modal/AssignAttorneyModal.tsx +++ b/user-interface/src/staff-assignment/modal/AssignAttorneyModal.tsx @@ -90,8 +90,12 @@ function _AssignAttorneyModal( if (showProps.bCase) { setBCase(showProps.bCase); if (showProps.bCase.assignments) { - setCheckListValues([...showProps.bCase.assignments]); - setPreviouslySelectedList([...showProps.bCase.assignments]); + const attorneys: AttorneyUser[] = []; + showProps.bCase.assignments.forEach((assignment) => { + attorneys.push({ id: assignment.userId, name: assignment.name } as AttorneyUser); + }); + setCheckListValues(attorneys); + setPreviouslySelectedList(attorneys); } if (showProps.callback) { submitCallbackRef.current = showProps.callback; diff --git a/user-interface/src/staff-assignment/row/StaffAssignmentRow.internal.test.ts b/user-interface/src/staff-assignment/row/StaffAssignmentRow.internal.test.ts index b5f3062d2..149fa68c0 100644 --- a/user-interface/src/staff-assignment/row/StaffAssignmentRow.internal.test.ts +++ b/user-interface/src/staff-assignment/row/StaffAssignmentRow.internal.test.ts @@ -1,14 +1,19 @@ import MockData from '@common/cams/test-utilities/mock-data'; -import Api2 from '@/lib/models/api2'; import TestingUtilities from '@/lib/testing/testing-utilities'; import Internal from './StaffAssignmentRow.internal'; // TODO: Find an alternative waitFor so we can stop using react testing library for non-React code. import { waitFor } from '@testing-library/react'; +import { CaseAssignment } from '@common/cams/assignments'; +import { CamsRole } from '@common/cams/roles'; describe('StaffAssignmentRowInternal', () => { - const bCase = MockData.getCaseBasics(); - const caseAssignments = [MockData.getAttorneyAssignment()]; + const caseId = 'testCaseId'; + const caseAssignments = [ + MockData.getAttorneyAssignment({ id: 'testAssignmentId', caseId, unassignedOn: undefined }), + ]; + const bCase = MockData.getCaseBasics({ override: { caseId, assignments: caseAssignments } }); + const mappedCaseAssignments = caseAssignments.map((assignment) => { return { id: assignment.userId, name: assignment.name }; }); @@ -28,56 +33,39 @@ describe('StaffAssignmentRowInternal', () => { TestingUtilities.spyOnUseState(); const globalAlert = TestingUtilities.spyOnGlobalAlert(); - const apiGetCaseAssignments = vi.spyOn(Api2, 'getCaseAssignments').mockResolvedValue({ - data: caseAssignments, - }); - beforeEach(() => { vi.clearAllMocks(); }); - test('should get attorneys assigned to the case', async () => { - const { state, actions } = Internal.useStateActions(initialState); - actions.getCaseAssignments(); - - await waitFor(() => { - expect(apiGetCaseAssignments).toHaveBeenCalledWith(bCase.caseId); - expect(state.assignments).toEqual(mappedCaseAssignments); - return true; - }); - }); - - test('should show an error message if getting attorney assignments fails', async () => { - apiGetCaseAssignments.mockRejectedValue('some error'); - + test('should handle successful assignment update', async () => { const { state, actions } = Internal.useStateActions(initialState); - actions.getCaseAssignments(); - await waitFor(() => { - expect(apiGetCaseAssignments).toHaveBeenCalledWith(bCase.caseId); - expect(state.assignments).toEqual([]); - expect(globalAlert.error).toHaveBeenCalledWith( - `Could not get staff assignments for case ${state.bCase.caseTitle}`, - ); - return true; - }); - }); + const attorney = MockData.getAttorneyUser(); - test('should handle successful assignment update', async () => { - const { state, actions } = Internal.useStateActions(initialState); - const endingAssigments = [MockData.getAttorneyUser()]; + // This is a Partial because in the implementation we + // `as CaseAssignment` a partial object literal for the dirty buffer. + const endingAssignment: Partial = { + userId: attorney.id, + name: attorney.name, + documentType: 'ASSIGNMENT', + caseId, + role: CamsRole.TrialAttorney, + }; + const endingAssignments = [endingAssignment]; actions.updateAssignmentsCallback({ status: 'success', apiResult: {}, bCase, previouslySelectedList: mappedCaseAssignments, - selectedAttorneyList: endingAssigments, + selectedAttorneyList: endingAssignments.map((assignment) => { + return { id: assignment.userId!, name: assignment.name! }; + }), }); await waitFor(() => { expect(globalAlert.success).toHaveBeenCalled(); - expect(state.assignments).toEqual(endingAssigments); + expect(state.assignments).toEqual(expect.arrayContaining(endingAssignments)); return true; }); }); @@ -100,7 +88,7 @@ describe('StaffAssignmentRowInternal', () => { await waitFor(() => { expect(globalAlert.error).toHaveBeenCalledWith(errorMessage); - expect(state.assignments).toEqual(startingAssignments); + expect(state.assignments).toEqual(expect.arrayContaining(startingAssignments)); }); }); }); diff --git a/user-interface/src/staff-assignment/row/StaffAssignmentRow.internal.ts b/user-interface/src/staff-assignment/row/StaffAssignmentRow.internal.ts index 7aca47162..8d02ac629 100644 --- a/user-interface/src/staff-assignment/row/StaffAssignmentRow.internal.ts +++ b/user-interface/src/staff-assignment/row/StaffAssignmentRow.internal.ts @@ -1,20 +1,20 @@ -import { useApi2 } from '@/lib/hooks/UseApi2'; import Actions from '@common/cams/actions'; import { CallbackProps } from '../modal/AssignAttorneyModal'; import { useGlobalAlert } from '@/lib/hooks/UseGlobalAlert'; import { getCaseNumber } from '@/lib/utils/caseNumber'; -import { AttorneyUser } from '@common/cams/users'; import { CaseBasics } from '@common/cams/cases'; import { useState } from '@/lib/hooks/UseState'; +import { CaseAssignment } from '@common/cams/assignments'; +import { CamsRole } from '@common/cams/roles'; type State = { - assignments: AttorneyUser[]; + // TODO: Make assignments a partial of CaseAssignment?? Include on the fields the UI needs to render and the modal needs to make assignments. + assignments: CaseAssignment[]; isLoading: boolean; bCase: CaseBasics; }; type Actions = { - getCaseAssignments: () => void; updateAssignmentsCallback: (props: CallbackProps) => Promise; }; @@ -22,7 +22,6 @@ function useStateActions(initialState: State): { state: State; actions: Actions; } { - const api = useApi2(); const globalAlert = useGlobalAlert(); const [state, setState] = useState(initialState); @@ -54,27 +53,21 @@ function useStateActions(initialState: State): { const message = messageArr.join(' case and ') + ` case ${getCaseNumber(bCase.caseId)} ${bCase.caseTitle}.`; - setState({ ...state, assignments: selectedAttorneyList }); + const assignments: CaseAssignment[] = selectedAttorneyList.map((attorney) => { + return { + userId: attorney.id, + name: attorney.name, + documentType: 'ASSIGNMENT', + caseId: bCase.caseId, + role: CamsRole.TrialAttorney, + } as CaseAssignment; + }); + setState({ ...state, assignments }); globalAlert?.success(message); } } - async function getCaseAssignments() { - api - .getCaseAssignments(state.bCase.caseId) - .then((response) => { - const assignments = response.data.map((assignment) => { - return { id: assignment.userId, name: assignment.name }; - }); - setState({ ...state, assignments, isLoading: false }); - }) - .catch((_reason) => { - globalAlert?.error(`Could not get staff assignments for case ${state.bCase.caseTitle}`); - setState({ ...state, isLoading: false }); - }); - } - - const actions = { updateAssignmentsCallback, getCaseAssignments }; + const actions = { updateAssignmentsCallback }; return { state, actions }; } diff --git a/user-interface/src/staff-assignment/row/StaffAssignmentRow.test.tsx b/user-interface/src/staff-assignment/row/StaffAssignmentRow.test.tsx index cfeb4f84e..604f6b006 100644 --- a/user-interface/src/staff-assignment/row/StaffAssignmentRow.test.tsx +++ b/user-interface/src/staff-assignment/row/StaffAssignmentRow.test.tsx @@ -11,7 +11,6 @@ import { formatDate } from '@/lib/utils/datetime'; import { UswdsButtonStyle } from '@/lib/components/uswds/Button'; import { CaseBasics } from '@common/cams/cases'; import Actions, { ResourceActions } from '@common/cams/actions'; -import TestingUtilities from '@/lib/testing/testing-utilities'; describe('StaffAssignmentRow tests', () => { const bCase: ResourceActions = { @@ -59,11 +58,8 @@ describe('StaffAssignmentRow tests', () => { test('should render a row', async () => { const assignedAttorney = MockData.getAttorneyAssignment(); - const caseAssignmentSpy = vi.spyOn(Api2, 'getCaseAssignments').mockResolvedValue({ - data: [assignedAttorney], - }); - renderWithProps(); + renderWithProps({ bCase: { ...bCase, assignments: [assignedAttorney] } }); await waitFor(() => { const firstTable = document.querySelector('table'); @@ -76,13 +72,10 @@ describe('StaffAssignmentRow tests', () => { expect(rows?.[3]).toHaveTextContent(formatDate(bCase.dateFiled)); expect(rows?.[4]).toHaveTextContent(assignedAttorney.name); expect(screen.getByTestId('attorney-list-0')).toBeVisible(); - expect(caseAssignmentSpy).toHaveBeenCalledWith(bCase.caseId); }); }); test('should show "unassigned" if there is not an assigned attorney', async () => { - vi.spyOn(Api2, 'getCaseAssignments').mockResolvedValue({ data: [] }); - renderWithProps(); await waitFor(() => { @@ -98,10 +91,12 @@ describe('StaffAssignmentRow tests', () => { }); test('should show assigned attorney names for assigned attorneys', async () => { - const assignments = MockData.buildArray(MockData.getAttorneyAssignment, 2); - vi.spyOn(Api2, 'getCaseAssignments').mockResolvedValue({ data: assignments }); + const assignments = MockData.buildArray( + () => MockData.getAttorneyAssignment({ caseId: bCase.caseId }), + 2, + ); - renderWithProps(); + renderWithProps({ bCase: { ...bCase, assignments } }); await waitFor(() => { const firstTable = document.querySelector('table'); @@ -118,10 +113,12 @@ describe('StaffAssignmentRow tests', () => { }); test('should not show assign/edit button', async () => { - const assignments = MockData.buildArray(MockData.getAttorneyAssignment, 2); - vi.spyOn(Api2, 'getCaseAssignments').mockResolvedValue({ data: assignments }); + const assignments = MockData.buildArray( + () => MockData.getAttorneyAssignment({ caseId: bCase.caseId }), + 2, + ); - const myCase = { ...bCase, _actions: [] }; + const myCase = { ...bCase, _actions: [], assignments }; renderWithProps({ bCase: myCase }); await waitFor(() => { @@ -135,30 +132,15 @@ describe('StaffAssignmentRow tests', () => { }); }); - test('should show error alert when an error is thrown by api.getCaseAssignments', async () => { - vi.spyOn(Api2, 'getCaseAssignments').mockRejectedValue('some error'); - const globalAlertSpy = TestingUtilities.spyOnGlobalAlert(); - - renderWithProps(); - - await waitFor(() => { - expect(globalAlertSpy.error).toHaveBeenCalledWith( - expect.stringContaining('Could not get staff assignments for case '), - ); - }); - }); - test('should render a list of assigned attorneys', async () => { const assignments = [MockData.getAttorneyAssignment()]; - vi.spyOn(Api2, 'getCaseAssignments').mockResolvedValue({ data: assignments }); - renderWithProps(); + renderWithProps({ bCase: { ...bCase, assignments } }); let staffList; await waitFor( () => { staffList = document.querySelector('.attorney-list-container'); - expect(staffList?.querySelector('.loading-spinner')).not.toBeInTheDocument(); }, { timeout: 2000 }, ); diff --git a/user-interface/src/staff-assignment/row/StaffAssignmentRow.tsx b/user-interface/src/staff-assignment/row/StaffAssignmentRow.tsx index 14d3e18c0..132812e92 100644 --- a/user-interface/src/staff-assignment/row/StaffAssignmentRow.tsx +++ b/user-interface/src/staff-assignment/row/StaffAssignmentRow.tsx @@ -1,16 +1,15 @@ -import { useEffect, useRef } from 'react'; +import { useRef } from 'react'; import { TableRow, TableRowData } from '@/lib/components/uswds/Table'; import { OpenModalButton } from '@/lib/components/uswds/modal/OpenModalButton'; import { CaseNumber } from '@/lib/components/CaseNumber'; import { formatDate } from '@/lib/utils/datetime'; import { UswdsButtonStyle } from '@/lib/components/uswds/Button'; -import { LoadingSpinner } from '@/lib/components/LoadingSpinner'; import Actions from '@common/cams/actions'; import { SearchResultsRowProps } from '@/search-results/SearchResults'; import { AssignAttorneyModalRef, CallbackProps } from '../modal/AssignAttorneyModal'; -import { AttorneyUser } from '@common/cams/users'; import Internal from './StaffAssignmentRow.internal'; -import { OpenModalButtonRef } from '../../lib/components/uswds/modal/modal-refs'; +import { OpenModalButtonRef } from '@/lib/components/uswds/modal/modal-refs'; +import { CaseAssignment } from '@common/cams/assignments'; export type StaffAssignmentRowOptions = { modalId: string; @@ -26,7 +25,7 @@ export function StaffAssignmentRow(props: StaffAssignmentRowProps) { const { modalId, modalRef } = options as StaffAssignmentRowOptions; const initialState = { - assignments: [], + assignments: bCase.assignments ?? [], isLoading: true, bCase, modalRef, @@ -35,17 +34,13 @@ export function StaffAssignmentRow(props: StaffAssignmentRowProps) { const openAssignmentsModalButtonRef = useRef(null); const { state, actions } = Internal.useStateActions(initialState); - useEffect(() => { - actions.getCaseAssignments(); - }, []); - function handleCallback(props: CallbackProps) { actions.updateAssignmentsCallback(props).then(() => { modalRef.current?.hide(); }); } - function buildActionButton(assignments: AttorneyUser[]) { + function buildActionButton(assignments: CaseAssignment[] | undefined) { const commonModalButtonProps = { className: 'case-assignment-modal-toggle', buttonIndex: `${idx}`, @@ -58,7 +53,7 @@ export function StaffAssignmentRow(props: StaffAssignmentRowProps) { ref: openAssignmentsModalButtonRef, }; - if (assignments.length > 0) { + if (assignments && assignments.length > 0) { return ( 0) { + function buildAssignmentList(assignments: Partial[] | undefined) { + if (assignments && assignments.length > 0) { return state.assignments?.map((attorney, key: number) => ( {attorney.name} @@ -102,22 +97,13 @@ export function StaffAssignmentRow(props: StaffAssignmentRowProps) { {formatDate(bCase.dateFiled)} Assigned Attorney: - {state.isLoading && ( -
-
- -
-
- )} - {!state.isLoading && ( -
-
{buildAssignmentList(state.assignments)}
-
- {Actions.contains(bCase, Actions.ManageAssignments) && - buildActionButton(state.assignments)} -
+
+
{buildAssignmentList(state.assignments)}
+
+ {Actions.contains(bCase, Actions.ManageAssignments) && + buildActionButton(state.assignments)}
- )} +
);