diff --git a/backend/functions/lib/adapters/gateways/case.assignment.cosmosdb.repository.test.ts b/backend/functions/lib/adapters/gateways/case.assignment.cosmosdb.repository.test.ts index ee52b970c6..3ece53ef28 100644 --- a/backend/functions/lib/adapters/gateways/case.assignment.cosmosdb.repository.test.ts +++ b/backend/functions/lib/adapters/gateways/case.assignment.cosmosdb.repository.test.ts @@ -32,7 +32,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: clairHuxtable, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; const testCaseAttorneyAssignment2: CaseAssignment = { documentType: 'ASSIGNMENT', @@ -41,7 +42,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: perryMason, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; const assignmentId1 = await repository.createAssignment(testCaseAttorneyAssignment1); @@ -75,7 +77,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: clairHuxtable, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; const actualIdResponse = await repository.createAssignment(testCaseAttorneyAssignment); @@ -98,7 +101,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: clairHuxtable, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; await repository.createAssignment(existingCaseAttorneyAssignment); @@ -110,7 +114,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: benMatlock, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; await expect(repository.updateAssignment(testCaseAttorneyAssignment)).rejects.toThrow( @@ -128,7 +133,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: clairHuxtable, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; const testCaseAttorneyAssignment2: CaseAssignment = { documentType: 'ASSIGNMENT', @@ -137,7 +143,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: perryMason, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; const assignmentId1 = await repository.createAssignment(testCaseAttorneyAssignment1); @@ -176,7 +183,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: benMatlock, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; await expect(repository.createAssignment(testCaseAttorneyAssignment)).rejects.toThrow( @@ -222,7 +230,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: perryMason, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; const testCaseAttorneyAssignment2: CaseAssignment = { documentType: 'ASSIGNMENT', @@ -231,7 +240,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: benMatlock, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; const caseIdTwo = randomUUID(); @@ -242,7 +252,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: clairHuxtable, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; const testCaseAttorneyAssignment4: CaseAssignment = { documentType: 'ASSIGNMENT', @@ -251,7 +262,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: perryMason, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; const caseIdThree = randomUUID(); @@ -262,7 +274,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: clairHuxtable, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; const testCaseAttorneyAssignment6: CaseAssignment = { documentType: 'ASSIGNMENT', @@ -271,7 +284,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: benMatlock, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; await repository.createAssignment(testCaseAttorneyAssignment1); @@ -362,7 +376,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: clairHuxtable, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; await expect(repository.createAssignment(existingCaseAttorneyAssignment)).rejects.toThrow( @@ -379,7 +394,8 @@ describe('Test case assignment cosmosdb repository tests', () => { name: benMatlock, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }; await expect(repository.updateAssignment(testCaseAttorneyAssignment)).rejects.toThrow( diff --git a/backend/functions/lib/adapters/gateways/cases.cosmosdb.repository.test.ts b/backend/functions/lib/adapters/gateways/cases.cosmosdb.repository.test.ts index 45a54bdf33..4d6e8f329b 100644 --- a/backend/functions/lib/adapters/gateways/cases.cosmosdb.repository.test.ts +++ b/backend/functions/lib/adapters/gateways/cases.cosmosdb.repository.test.ts @@ -12,6 +12,7 @@ import { import { AggregateAuthenticationError } from '@azure/identity'; import { CaseAssignmentHistory } from '../../../../../common/src/cams/history'; import { MockData } from '../../../../../common/src/cams/test-utilities/mock-data'; +import { SYSTEM_USER_REFERENCE } from '../../../../../common/src/cams/auditable'; describe('Runtime State Repo', () => { const caseId1 = '111-11-11111'; @@ -122,7 +123,8 @@ describe('Test case history cosmosdb repository tests', () => { const testCaseAssignmentHistory: CaseAssignmentHistory = { caseId, documentType: 'AUDIT_ASSIGNMENT', - occurredAtTimestamp: new Date().toISOString(), + updatedOn: new Date().toISOString(), + updatedBy: SYSTEM_USER_REFERENCE, before: [], after: [], }; @@ -137,7 +139,8 @@ describe('Test case history cosmosdb repository tests', () => { const testCaseAssignmentHistory: CaseAssignmentHistory = { caseId, documentType: 'AUDIT_ASSIGNMENT', - occurredAtTimestamp: new Date().toISOString(), + updatedBy: SYSTEM_USER_REFERENCE, + updatedOn: new Date().toISOString(), before: [], after: [], }; diff --git a/backend/functions/lib/adapters/gateways/cases.cosmosdb.repository.ts b/backend/functions/lib/adapters/gateways/cases.cosmosdb.repository.ts index 8654d53a96..9bc4d1e105 100644 --- a/backend/functions/lib/adapters/gateways/cases.cosmosdb.repository.ts +++ b/backend/functions/lib/adapters/gateways/cases.cosmosdb.repository.ts @@ -110,9 +110,6 @@ export class CasesCosmosDbRepository implements CasesRepository { async createCaseHistory(context: ApplicationContext, history: CaseHistory): Promise { try { - if (!history.occurredAtTimestamp) { - history.occurredAtTimestamp = new Date().toISOString(); - } const { resource } = await this.cosmosDbClient .database(this.cosmosConfig.databaseName) .container(this.containerName) diff --git a/backend/functions/lib/adapters/gateways/consolidations.cosmosdb.repository.ts b/backend/functions/lib/adapters/gateways/consolidations.cosmosdb.repository.ts index c8ac8d1ed0..8620851bd5 100644 --- a/backend/functions/lib/adapters/gateways/consolidations.cosmosdb.repository.ts +++ b/backend/functions/lib/adapters/gateways/consolidations.cosmosdb.repository.ts @@ -1,3 +1,4 @@ +import { OrdersSearchPredicate } from '../../../../../common/src/api/search'; import { ConsolidationOrder } from '../../../../../common/src/cams/orders'; import { ConsolidationOrdersRepository } from '../../use-cases/gateways.types'; import { ApplicationContext } from '../types/basic'; @@ -13,11 +14,30 @@ export default class ConsolidationOrdersCosmosDbRepository constructor(context: ApplicationContext) { super(context, CONTAINER_NAME, MODULE_NAME); } - public async getAll(context: ApplicationContext): Promise> { - const querySpec = { - query: 'SELECT * FROM c ORDER BY c.orderDate ASC', - parameters: [], - }; + public async search( + context: ApplicationContext, + predicate?: OrdersSearchPredicate, + ): Promise> { + let querySpec; + if (!predicate) { + querySpec = { + query: 'SELECT * FROM c ORDER BY c.orderDate ASC', + parameters: [], + }; + } else { + // TODO: Sanitize the inputs + // Group designator comes from local-storage-gateway and is store in the user session cache. + // We get associated division codes from DXTR and also store that in the session cache. + // We are not ever trusting the client with this information as of 9 Sept 2024. + const whereClause = + 'WHERE ' + + predicate.divisionCodes.map((dCode) => `c.courtDivisionCode='${dCode}'`).join(' OR ') + + ' ORDER BY c.orderDate ASC'; + querySpec = { + query: 'SELECT * FROM c ' + whereClause, + parameters: [], + }; + } return await this.query(context, querySpec); } } diff --git a/backend/functions/lib/adapters/gateways/dxtr/orders.dxtr.gateway.ts b/backend/functions/lib/adapters/gateways/dxtr/orders.dxtr.gateway.ts index edc1caf485..08da5a0c80 100644 --- a/backend/functions/lib/adapters/gateways/dxtr/orders.dxtr.gateway.ts +++ b/backend/functions/lib/adapters/gateways/dxtr/orders.dxtr.gateway.ts @@ -409,6 +409,7 @@ export class DxtrOrdersGateway implements OrdersGateway { CS.CS_SHORT_TITLE AS caseTitle, CS.CS_CHAPTER AS chapter, C.COURT_NAME AS courtName, + CS.CS_DIV as courtDivisionCode, O.OFFICE_NAME_DISPLAY AS courtDivisionName, G.REGION_ID AS regionId, R.REGION_NAME AS regionName, diff --git a/backend/functions/lib/adapters/gateways/orders.cosmosdb.repository.test.ts b/backend/functions/lib/adapters/gateways/orders.cosmosdb.repository.test.ts index f7b95a2a9a..c7c90dea13 100644 --- a/backend/functions/lib/adapters/gateways/orders.cosmosdb.repository.test.ts +++ b/backend/functions/lib/adapters/gateways/orders.cosmosdb.repository.test.ts @@ -48,7 +48,7 @@ describe('Test case assignment cosmosdb repository tests', () => { resources: mockOrders, }); - const testResult = await repository.getOrders(applicationContext); + const testResult = await repository.search(applicationContext); expect(testResult).toEqual(mockOrders); expect(mockFetchAll).toHaveBeenCalled(); @@ -227,7 +227,7 @@ describe('Test case assignment cosmosdb repository tests', () => { jest.spyOn(MockHumbleItems.prototype, 'create').mockRejectedValue(aggregateError); jest.spyOn(MockHumbleItem.prototype, 'read').mockRejectedValue(aggregateError); - await expect(repository.getOrders(applicationContext)).rejects.toThrow(serverConfigError); + await expect(repository.search(applicationContext)).rejects.toThrow(serverConfigError); await expect( repository.getOrder(applicationContext, testNewOrderData.id, testNewOrderData.caseId), ).rejects.toThrow(serverConfigError); @@ -245,7 +245,7 @@ describe('Test case assignment cosmosdb repository tests', () => { jest.spyOn(MockHumbleItem.prototype, 'read').mockRejectedValue(error); jest.spyOn(MockHumbleItems.prototype, 'create').mockRejectedValue(error); - await expect(repository.getOrders(applicationContext)).rejects.toThrow(error); + await expect(repository.search(applicationContext)).rejects.toThrow(error); await expect( repository.getOrder(applicationContext, testNewOrderData.id, testNewOrderData.caseId), ).rejects.toThrow(error); diff --git a/backend/functions/lib/adapters/gateways/orders.cosmosdb.repository.ts b/backend/functions/lib/adapters/gateways/orders.cosmosdb.repository.ts index 17c2a36530..34baa59638 100644 --- a/backend/functions/lib/adapters/gateways/orders.cosmosdb.repository.ts +++ b/backend/functions/lib/adapters/gateways/orders.cosmosdb.repository.ts @@ -10,6 +10,7 @@ import { Order, TransferOrder, TransferOrderAction } from '../../../../../common import CosmosClientHumble from '../../cosmos-humble-objects/cosmos-client-humble'; import { MockHumbleClient } from '../../testing/mock.cosmos-client-humble'; import { QueryOptions } from '../../cosmos-humble-objects/cosmos-items-humble'; +import { OrdersSearchPredicate } from '../../../../../common/src/api/search'; const MODULE_NAME: string = 'COSMOS_DB_REPOSITORY_ORDERS'; const CONTAINER_NAME: string = 'orders'; @@ -32,12 +33,27 @@ export class OrdersCosmosDbRepository implements OrdersRepository { this.moduleName = moduleName; } - async getOrders(context: ApplicationContext): Promise { - const query = 'SELECT * FROM c ORDER BY c.orderDate ASC'; - const querySpec = { - query, - parameters: [], - }; + async search(context: ApplicationContext, predicate?: OrdersSearchPredicate): Promise { + let querySpec; + if (!predicate) { + querySpec = { + query: 'SELECT * FROM c ORDER BY c.orderDate ASC', + parameters: [], + }; + } else { + // TODO: Sanitize the inputs + // Group designator comes from local-storage-gateway and is store in the user session cache. + // We get associated division codes from DXTR and also store that in the session cache. + // We are not ever trusting the client with this information as of 9 Sept 2024. + const whereClause = + 'WHERE ' + + predicate.divisionCodes.map((dCode) => `c.courtDivisionCode='${dCode}'`).join(' OR ') + + ' ORDER BY c.orderDate ASC'; + querySpec = { + query: 'SELECT * FROM c ' + whereClause, + parameters: [], + }; + } const response = await this.queryData(context, querySpec); return response; } diff --git a/backend/functions/lib/adapters/gateways/storage/local-storage-gateway.test.ts b/backend/functions/lib/adapters/gateways/storage/local-storage-gateway.test.ts index c6fa025337..2cb4a0d5d3 100644 --- a/backend/functions/lib/adapters/gateways/storage/local-storage-gateway.test.ts +++ b/backend/functions/lib/adapters/gateways/storage/local-storage-gateway.test.ts @@ -1,4 +1,8 @@ -import LocalStorageGateway, { OFFICE_MAPPING_PATH } from './local-storage-gateway'; +import LocalStorageGateway, { + OFFICE_MAPPING_PATH, + ROLE_MAPPING_PATH, +} from './local-storage-gateway'; +import { CamsRole } from '../../../../../../common/src/cams/roles'; describe('map get', () => { test('should return appropriate string for valid path', () => { @@ -8,4 +12,19 @@ describe('map get', () => { test('should return null for invalid path', () => { expect(LocalStorageGateway.get('INVALID_PATH')).toBeNull(); }); + + test('should include case assignment manager role', () => { + const roleMap = LocalStorageGateway.get(ROLE_MAPPING_PATH); + expect(roleMap).toContain(CamsRole.CaseAssignmentManager); + }); + + test('should include trial attorney role', () => { + const roleMap = LocalStorageGateway.get(ROLE_MAPPING_PATH); + expect(roleMap).toContain(CamsRole.TrialAttorney); + }); + + test('should include data verification role', () => { + const roleMap = LocalStorageGateway.get(ROLE_MAPPING_PATH); + expect(roleMap).toContain(CamsRole.DataVerifier); + }); }); diff --git a/backend/functions/lib/adapters/gateways/storage/local-storage-gateway.ts b/backend/functions/lib/adapters/gateways/storage/local-storage-gateway.ts index 4162cc186a..d3ba758490 100644 --- a/backend/functions/lib/adapters/gateways/storage/local-storage-gateway.ts +++ b/backend/functions/lib/adapters/gateways/storage/local-storage-gateway.ts @@ -8,7 +8,8 @@ export const ROLE_MAPPING_PATH = '/rolemapping.csv'; const ROLE_MAPPING = 'ad_group_name,idp_group_name,cams_role\n' + 'USTP_CAMS_Case_assignment_Manager,USTP CAMS Case Assignment Manager,CaseAssignmentManager\n' + - 'USTP_CAMS_Trial_Attorney,USTP CAMS Trial Attorney,TrialAttorney\n'; + 'USTP_CAMS_Trial_Attorney,USTP CAMS Trial Attorney,TrialAttorney\n' + + 'USTP_CAMS_Data_Verifier,USTP CAMS Data Verifier,DataVerifier\n'; export const OFFICE_MAPPING_PATH = '/officemapping.csv'; const OFFICE_MAPPING = diff --git a/backend/functions/lib/controllers/orders/orders.controller.ts b/backend/functions/lib/controllers/orders/orders.controller.ts index 76f18dffc5..6e0791ddb9 100644 --- a/backend/functions/lib/controllers/orders/orders.controller.ts +++ b/backend/functions/lib/controllers/orders/orders.controller.ts @@ -60,7 +60,7 @@ export class OrdersController implements CamsController { return this.handleConsolidations(context); case 'orders': return this.handleOrders(context); - case 'order-manual-sync': + case 'orders-sync': return this.handleOrderSync(context); case 'orders-suggestions': return this.handleOrdersSuggestions(context); diff --git a/backend/functions/lib/cosmos-humble-objects/fake.assignments.cosmos-client-humble.ts b/backend/functions/lib/cosmos-humble-objects/fake.assignments.cosmos-client-humble.ts index 8e89fcd2f2..a50d2e07c6 100644 --- a/backend/functions/lib/cosmos-humble-objects/fake.assignments.cosmos-client-humble.ts +++ b/backend/functions/lib/cosmos-humble-objects/fake.assignments.cosmos-client-humble.ts @@ -46,7 +46,8 @@ export default class FakeAssignmentsCosmosClientHumble { name: 'Test Attorney', role: 'TrialAttorney', assignedOn: '2024-03-05', - changedBy: MockData.getCamsUserReference(), + updatedOn: '2024-03-05', + updatedBy: MockData.getCamsUserReference(), }; } assignment.id = `assignment-id-${Math.round(Math.random() * 1000)}`; diff --git a/backend/functions/lib/testing/local-data/local-consolidation-orders-repository.ts b/backend/functions/lib/testing/local-data/local-consolidation-orders-repository.ts index 7a874398aa..41dd504236 100644 --- a/backend/functions/lib/testing/local-data/local-consolidation-orders-repository.ts +++ b/backend/functions/lib/testing/local-data/local-consolidation-orders-repository.ts @@ -2,12 +2,16 @@ import { ConsolidationOrder } from '../../../../../common/src/cams/orders'; import { ApplicationContext } from '../../adapters/types/basic'; import { ConsolidationOrdersRepository } from '../../use-cases/gateways.types'; import { LocalCosmosDbRepository } from './local-cosmos-db-repository'; +import { OrdersSearchPredicate } from '../../../../../common/src/api/search'; export class LocalConsolidationOrdersRepository extends LocalCosmosDbRepository implements ConsolidationOrdersRepository { - async getAll(_context: ApplicationContext): Promise { + async search( + _context: ApplicationContext, + _predicate?: OrdersSearchPredicate, + ): Promise { return [...this.container]; } } diff --git a/backend/functions/lib/testing/mock-data/case-history.mock.ts b/backend/functions/lib/testing/mock-data/case-history.mock.ts index b1c4d82b86..98d4517116 100644 --- a/backend/functions/lib/testing/mock-data/case-history.mock.ts +++ b/backend/functions/lib/testing/mock-data/case-history.mock.ts @@ -1,3 +1,4 @@ +import { SYSTEM_USER_REFERENCE } from '../../../../../common/src/cams/auditable'; import { CaseAssignmentHistory } from '../../../../../common/src/cams/history'; import MockData from '../../../../../common/src/cams/test-utilities/mock-data'; @@ -6,7 +7,8 @@ export const CASE_HISTORY: CaseAssignmentHistory[] = [ id: 'da2ba8c0-b38b-4b4b-a4d7-986e0fdb671a', caseId: '081-22-84687', documentType: 'AUDIT_ASSIGNMENT', - occurredAtTimestamp: '2023-12-14T21:39:18.909Z', + updatedOn: '2023-12-14T21:39:18.909Z', + updatedBy: SYSTEM_USER_REFERENCE, before: [], after: [ { @@ -17,7 +19,8 @@ export const CASE_HISTORY: CaseAssignmentHistory[] = [ name: 'Susan Arbeit', role: 'TrialAttorney', assignedOn: '2023-12-14T21:39:18.909Z', - changedBy: MockData.getCamsUserReference(), + updatedOn: '2023-12-14T21:39:18.909Z', + updatedBy: MockData.getCamsUserReference(), }, { id: 'de6f4277-b7a8-4d90-bdb7-59a52d14d895', @@ -27,7 +30,8 @@ export const CASE_HISTORY: CaseAssignmentHistory[] = [ name: 'Mark Bruh', role: 'TrialAttorney', assignedOn: '2023-12-14T21:39:18.909Z', - changedBy: MockData.getCamsUserReference(), + updatedOn: '2023-12-14T21:39:18.909Z', + updatedBy: MockData.getCamsUserReference(), }, { id: 'a2cb1c7c-2c1f-412d-bd0a-c1b412769de1', @@ -37,7 +41,8 @@ export const CASE_HISTORY: CaseAssignmentHistory[] = [ name: 'Shara Cornell', role: 'TrialAttorney', assignedOn: '2023-12-14T21:39:18.909Z', - changedBy: MockData.getCamsUserReference(), + updatedOn: '2023-12-14T21:39:18.909Z', + updatedBy: MockData.getCamsUserReference(), }, ], }, @@ -45,7 +50,8 @@ export const CASE_HISTORY: CaseAssignmentHistory[] = [ id: '72515281-2ac9-49f6-b40c-affcc12ec059', caseId: '081-22-84687', documentType: 'AUDIT_ASSIGNMENT', - occurredAtTimestamp: '2023-12-14T21:39:26.755Z', + updatedOn: '2023-12-14T21:39:18.909Z', + updatedBy: SYSTEM_USER_REFERENCE, before: [ { id: '2265215f-88e8-4cc7-a2d1-2d5ebaaabe73', @@ -55,7 +61,8 @@ export const CASE_HISTORY: CaseAssignmentHistory[] = [ name: 'Susan Arbeit', role: 'TrialAttorney', assignedOn: '2023-12-14T21:39:18.909Z', - changedBy: MockData.getCamsUserReference(), + updatedOn: '2023-12-14T21:39:18.909Z', + updatedBy: MockData.getCamsUserReference(), }, { id: 'de6f4277-b7a8-4d90-bdb7-59a52d14d895', @@ -65,7 +72,8 @@ export const CASE_HISTORY: CaseAssignmentHistory[] = [ name: 'Mark Bruh', role: 'TrialAttorney', assignedOn: '2023-12-14T21:39:18.909Z', - changedBy: MockData.getCamsUserReference(), + updatedOn: '2023-12-14T21:39:18.909Z', + updatedBy: MockData.getCamsUserReference(), }, { id: 'a2cb1c7c-2c1f-412d-bd0a-c1b412769de1', @@ -75,7 +83,8 @@ export const CASE_HISTORY: CaseAssignmentHistory[] = [ name: 'Shara Cornell', role: 'TrialAttorney', assignedOn: '2023-12-14T21:39:18.909Z', - changedBy: MockData.getCamsUserReference(), + updatedOn: '2023-12-14T21:39:18.909Z', + updatedBy: MockData.getCamsUserReference(), }, ], after: [ @@ -85,9 +94,10 @@ export const CASE_HISTORY: CaseAssignmentHistory[] = [ userId: 'userId-Shara Cornell', name: 'Shara Cornell', role: 'TrialAttorney', - assignedOn: '2023-12-14T21:39:18.909Z', id: 'a2cb1c7c-2c1f-412d-bd0a-c1b412769de1', - changedBy: MockData.getCamsUserReference(), + assignedOn: '2023-12-14T21:39:18.909Z', + updatedOn: '2023-12-14T21:39:18.909Z', + updatedBy: MockData.getCamsUserReference(), }, { documentType: 'ASSIGNMENT', @@ -95,9 +105,10 @@ export const CASE_HISTORY: CaseAssignmentHistory[] = [ userId: 'userId-Brian S Masumoto', name: 'Brian S Masumoto', role: 'TrialAttorney', - assignedOn: '2023-12-14T21:39:26.755Z', id: '8d101caa-e3c8-4927-8cf4-3a57a481bf9c', - changedBy: MockData.getCamsUserReference(), + assignedOn: '2023-12-14T21:39:26.755Z', + updatedOn: '2023-12-14T21:39:26.755Z', + updatedBy: MockData.getCamsUserReference(), }, { documentType: 'ASSIGNMENT', @@ -105,9 +116,10 @@ export const CASE_HISTORY: CaseAssignmentHistory[] = [ userId: 'userId-Daniel Rudewicz', name: 'Daniel Rudewicz', role: 'TrialAttorney', - assignedOn: '2023-12-14T21:39:26.755Z', id: '7a6e1dba-4a27-4c59-85bb-919a6746a676', - changedBy: MockData.getCamsUserReference(), + assignedOn: '2023-12-14T21:39:26.755Z', + updatedOn: '2023-12-14T21:39:26.755Z', + updatedBy: MockData.getCamsUserReference(), }, ], }, diff --git a/backend/functions/lib/testing/mock-gateways/mock-oauth2-gateway.ts b/backend/functions/lib/testing/mock-gateways/mock-oauth2-gateway.ts index b99fff5274..7b35c1e33b 100644 --- a/backend/functions/lib/testing/mock-gateways/mock-oauth2-gateway.ts +++ b/backend/functions/lib/testing/mock-gateways/mock-oauth2-gateway.ts @@ -57,6 +57,7 @@ export async function verifyToken(accessToken: string): Promise { function addSuperUserOffices(user: CamsUser) { if (user.roles.includes(CamsRole.SuperUser)) { user.offices = OFFICES; + user.roles = Object.values(CamsRole); } } diff --git a/backend/functions/lib/use-cases/case-assignment.ts b/backend/functions/lib/use-cases/case-assignment.ts index d010353244..17b3ec5d3c 100644 --- a/backend/functions/lib/use-cases/case-assignment.ts +++ b/backend/functions/lib/use-cases/case-assignment.ts @@ -7,8 +7,8 @@ import { CaseAssignmentHistory } from '../../../../common/src/cams/history'; import CaseManagement from './case-management'; import { CamsUserReference } from '../../../../common/src/cams/users'; import { CamsRole } from '../../../../common/src/cams/roles'; -import { getCamsUserReference } from '../../../../common/src/cams/session'; import { AssignmentError } from './assignment.exception'; +import { createAuditRecord } from '../../../../common/src/cams/auditable'; const MODULE_NAME = 'CASE-ASSIGNMENT'; @@ -70,15 +70,18 @@ export class CaseAssignmentUseCase { const attorneys = [...new Set(newAssignments)]; const currentDate = new Date().toISOString(); attorneys.forEach((attorney) => { - const assignment: CaseAssignment = { - documentType: 'ASSIGNMENT', - caseId: caseId, - userId: attorney.id, - name: attorney.name, - role: CamsRole[role], - assignedOn: currentDate, - changedBy: getCamsUserReference(context.session!.user), - }; + const assignment = createAuditRecord( + { + documentType: 'ASSIGNMENT', + caseId: caseId, + userId: attorney.id, + name: attorney.name, + role: CamsRole[role], + assignedOn: currentDate, + }, + context.session.user, + { updatedOn: currentDate }, + ); listOfAssignments.push(assignment); }); const listOfAssignmentIdsCreated: string[] = []; @@ -112,14 +115,16 @@ export class CaseAssignmentUseCase { } const newAssignmentRecords = await this.assignmentRepository.findAssignmentsByCaseId(caseId); - const history: CaseAssignmentHistory = { - caseId, - documentType: 'AUDIT_ASSIGNMENT', - occurredAtTimestamp: currentDate, - before: existingAssignmentRecords, - after: newAssignmentRecords, - changedBy: getCamsUserReference(context.session!.user), - }; + const history = createAuditRecord( + { + caseId, + documentType: 'AUDIT_ASSIGNMENT', + before: existingAssignmentRecords, + after: newAssignmentRecords, + }, + context.session.user, + { updatedOn: currentDate }, + ); await this.casesRepository.createCaseHistory(context, history); context.logger.info( diff --git a/backend/functions/lib/use-cases/case-management.test.ts b/backend/functions/lib/use-cases/case-management.test.ts index 24e9290601..674f3aecf7 100644 --- a/backend/functions/lib/use-cases/case-management.test.ts +++ b/backend/functions/lib/use-cases/case-management.test.ts @@ -25,7 +25,8 @@ const assignments: CaseAssignment[] = [ name: attorneyJaneSmith.name, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }, { documentType: 'ASSIGNMENT', @@ -35,7 +36,8 @@ const assignments: CaseAssignment[] = [ name: attorneyJoeNobel.name, role: CamsRole.TrialAttorney, assignedOn: currentDate, - changedBy: MockData.getCamsUserReference(), + updatedOn: currentDate, + updatedBy: MockData.getCamsUserReference(), }, ]; diff --git a/backend/functions/lib/use-cases/gateways.types.ts b/backend/functions/lib/use-cases/gateways.types.ts index cd3021f48e..c131d98c62 100644 --- a/backend/functions/lib/use-cases/gateways.types.ts +++ b/backend/functions/lib/use-cases/gateways.types.ts @@ -13,6 +13,7 @@ import { } from '../../../../common/src/cams/events'; import { CaseAssignmentHistory, CaseHistory } from '../../../../common/src/cams/history'; import { CaseDocket } from '../../../../common/src/cams/cases'; +import { OrdersSearchPredicate } from '../../../../common/src/api/search'; export interface RepositoryResource { id?: string; @@ -27,7 +28,10 @@ export interface DocumentRepository { } export interface ConsolidationOrdersRepository extends DocumentRepository { - getAll(context: ApplicationContext): Promise; + search( + context: ApplicationContext, + predicate?: OrdersSearchPredicate, + ): Promise; } export interface CaseDocketGateway { @@ -46,7 +50,7 @@ export interface OrdersGateway { } export interface OrdersRepository { - getOrders(context: ApplicationContext): Promise; + search(context: ApplicationContext, predicate?: OrdersSearchPredicate): Promise; getOrder(context: ApplicationContext, id: string, partitionKey: string): Promise; putOrders(context: ApplicationContext, orders: Order[]): Promise; updateOrder(context: ApplicationContext, id: string, data: TransferOrderAction); 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 31926072fc..837b94082e 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 @@ -22,6 +22,9 @@ import { CasesCosmosDbRepository } from '../../adapters/gateways/cases.cosmosdb. import * as crypto from 'crypto'; import { CaseHistory, ConsolidationOrderSummary } from '../../../../../common/src/cams/history'; import { CaseAssignmentUseCase } from '../case-assignment'; +import { CamsRole } from '../../../../../common/src/cams/roles'; +import { MANHATTAN } from '../../../../../common/src/cams/test-utilities/offices.mock'; +import { SYSTEM_USER_REFERENCE } from '../../../../../common/src/cams/auditable'; describe('Orders use case', () => { let mockContext; @@ -32,10 +35,14 @@ describe('Orders use case', () => { let casesGateway; let consolidationRepo; let useCase: OrdersUseCase; + const authorizedUser = MockData.getCamsUser({ + roles: [CamsRole.DataVerifier], + offices: [MANHATTAN], + }); beforeEach(async () => { mockContext = await createMockApplicationContext(); - mockContext.session = await createMockApplicationContextSession(); + mockContext.session = await createMockApplicationContextSession({ user: authorizedUser }); ordersGateway = getOrdersGateway(mockContext); runtimeStateRepo = getRuntimeStateRepository(mockContext); ordersRepo = getOrdersRepository(mockContext); @@ -130,7 +137,8 @@ describe('Orders use case', () => { caseId: pendingConsolidation.childCases[0].caseId, before: null, after: before, - occurredAtTimestamp: '2024-01-01T12:00:00.000Z', + updatedOn: '2024-01-01T12:00:00.000Z', + updatedBy: SYSTEM_USER_REFERENCE, }; const mockGetHistory = jest @@ -240,7 +248,8 @@ describe('Orders use case', () => { caseId: originalConsolidation.childCases[0].caseId, before: null, after: before, - occurredAtTimestamp: '2024-01-01T12:00:00.000Z', + updatedOn: '2024-01-01T12:00:00.000Z', + updatedBy: SYSTEM_USER_REFERENCE, }; const mockGetHistory = jest .spyOn(CasesCosmosDbRepository.prototype, 'getCaseHistory') diff --git a/backend/functions/lib/use-cases/orders/orders-local-gateway.test.ts b/backend/functions/lib/use-cases/orders/orders-local-gateway.test.ts index f65851cbb5..af1782faee 100644 --- a/backend/functions/lib/use-cases/orders/orders-local-gateway.test.ts +++ b/backend/functions/lib/use-cases/orders/orders-local-gateway.test.ts @@ -8,16 +8,21 @@ import { import { OrdersUseCase } from './orders'; import * as FactoryModule from '../../factory'; -import { CasesRepository, ConsolidationOrdersRepository } from '../gateways.types'; -import { ApplicationContext } from '../../adapters/types/basic'; -import { ConsolidationFrom, ConsolidationTo } from '../../../../../common/src/cams/events'; -import { createMockApplicationContext } from '../../testing/testing-utilities'; import { getCasesGateway, getOrdersGateway, getOrdersRepository, getRuntimeStateRepository, } from '../../factory'; +import { CasesRepository, ConsolidationOrdersRepository } from '../gateways.types'; +import { ApplicationContext } from '../../adapters/types/basic'; +import { ConsolidationFrom, ConsolidationTo } from '../../../../../common/src/cams/events'; +import { + createMockApplicationContext, + createMockApplicationContextSession, +} from '../../testing/testing-utilities'; +import { CamsRole } from '../../../../../common/src/cams/roles'; +import { MANHATTAN } from '../../../../../common/src/cams/test-utilities/offices.mock'; // TODO: This could be a testing library functions. function setupCasesRepoMock(repo: CasesRepository) { @@ -122,9 +127,14 @@ describe('orders use case tests', () => { let ordersRepo; let runtimeStateRepo; let casesGateway; + const authorizedUser = MockData.getCamsUser({ + roles: [CamsRole.DataVerifier], + offices: [MANHATTAN], + }); beforeEach(async () => { mockContext = await createMockApplicationContext(); + mockContext.session = await createMockApplicationContextSession({ user: authorizedUser }); ordersGateway = getOrdersGateway(mockContext); runtimeStateRepo = getRuntimeStateRepository(mockContext); ordersRepo = getOrdersRepository(mockContext); @@ -176,6 +186,4 @@ describe('orders use case tests', () => { expect(casesRepoSpy.createConsolidationTo).not.toHaveBeenCalled(); expect(casesRepoSpy.createConsolidationFrom).not.toHaveBeenCalled(); }); - - test('should not consolidate a case that has already been consolidated', () => {}); }); diff --git a/backend/functions/lib/use-cases/orders/orders.test.ts b/backend/functions/lib/use-cases/orders/orders.test.ts index 8a2d4e026a..2af871f6af 100644 --- a/backend/functions/lib/use-cases/orders/orders.test.ts +++ b/backend/functions/lib/use-cases/orders/orders.test.ts @@ -30,12 +30,15 @@ import { CasesLocalGateway } from '../../adapters/gateways/cases.local.gateway'; import { CaseSummary } from '../../../../../common/src/cams/cases'; import { ApplicationContext } from '../../adapters/types/basic'; import { NotFoundError } from '../../common-errors/not-found-error'; -import { sortDates } from '../../../../../common/src/date-helper'; import { CosmosDbRepository } from '../../adapters/gateways/cosmos/cosmos.repository'; import { CasesCosmosDbRepository } from '../../adapters/gateways/cases.cosmosdb.repository'; import * as crypto from 'crypto'; import { CaseHistory, ConsolidationOrderSummary } from '../../../../../common/src/cams/history'; import { MockOrdersGateway } from '../../testing/mock-gateways/mock.orders.gateway'; +import { CamsRole } from '../../../../../common/src/cams/roles'; +import { getCamsUserReference } from '../../../../../common/src/cams/session'; +import { CaseAssignmentUseCase } from '../case-assignment'; +import { MANHATTAN } from '../../../../../common/src/cams/test-utilities/offices.mock'; describe('Orders use case', () => { const CASE_ID = '000-11-22222'; @@ -47,10 +50,15 @@ describe('Orders use case', () => { let casesGateway; let consolidationRepo; let useCase: OrdersUseCase; + const authorizedUser = MockData.getCamsUser({ + roles: [CamsRole.DataVerifier], + offices: [MANHATTAN], + }); + const unauthorizedUser = MockData.getCamsUser({ roles: [] }); beforeEach(async () => { mockContext = await createMockApplicationContext(); - mockContext.session = await createMockApplicationContextSession(); + mockContext.session = await createMockApplicationContextSession({ user: authorizedUser }); ordersGateway = getOrdersGateway(mockContext); runtimeStateRepo = getRuntimeStateRepository(mockContext); ordersRepo = getOrdersRepository(mockContext); @@ -68,19 +76,32 @@ describe('Orders use case', () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.restoreAllMocks(); }); test('should return list of orders for the API from the repo', async () => { - const mockOrders = [MockData.getTransferOrder(), MockData.getConsolidationOrder()].sort( - (a, b) => sortDates(a.orderDate, b.orderDate), - ); - const mockRead = jest.spyOn(MockHumbleQuery.prototype, 'fetchAll').mockResolvedValueOnce({ - resources: mockOrders, + const mockTransfer1 = MockData.getTransferOrder({ override: { orderDate: '2024-08-01' } }); + const mockTransfer2 = MockData.getTransferOrder({ override: { orderDate: '2024-09-01' } }); + const mockTransferOrders = [mockTransfer1, mockTransfer2]; + const mockConsolidation1 = MockData.getConsolidationOrder({ + override: { orderDate: '2024-08-02' }, + }); + const mockConsolidation2 = MockData.getConsolidationOrder({ + override: { orderDate: '2024-09-02' }, }); + const mockConsolidationOrders = [mockConsolidation1, mockConsolidation2]; + + const orderRepoMock = jest.spyOn(ordersRepo, 'search').mockResolvedValue(mockTransferOrders); + const consolidationsRepoMock = jest + .spyOn(consolidationRepo, 'search') + .mockResolvedValue(mockConsolidationOrders); + + const divisionCodes = authorizedUser.offices.map((office) => office.courtDivisionCode); + const expectedResult = [mockTransfer1, mockConsolidation1, mockTransfer2, mockConsolidation2]; const result = await useCase.getOrders(mockContext); - expect(result).toEqual(mockOrders); - expect(mockRead).toHaveBeenCalled(); + expect(result).toEqual(expectedResult); + expect(orderRepoMock).toHaveBeenCalledWith(mockContext, { divisionCodes }); + expect(consolidationsRepoMock).toHaveBeenCalledWith(mockContext, { divisionCodes }); }); test('should return list of suggested cases for an order', async () => { @@ -358,7 +379,8 @@ describe('Orders use case', () => { caseId: pendingConsolidation.childCases[0].caseId, before: null, after: before, - occurredAtTimestamp: '2024-01-01T12:00:00.000Z', + updatedOn: '2024-01-01T12:00:00.000Z', + updatedBy: authorizedUser, }; const mockGetHistory = jest .spyOn(CasesCosmosDbRepository.prototype, 'getCaseHistory') @@ -450,4 +472,130 @@ describe('Orders use case', () => { ); expect(mockGetConsolidation).toHaveBeenCalled(); }); + + test('should throw an error if user is unauthorized to update transfers', async () => { + const order: TransferOrder = MockData.getTransferOrder({ override: { status: 'approved' } }); + + const action: TransferOrderAction = { + id: order.id, + orderType: 'transfer', + caseId: order.caseId, + newCase: order.newCase, + status: 'approved', + }; + + const updateOrderFn = jest + .spyOn(ordersRepo, 'updateOrder') + .mockResolvedValue({ id: 'mock-guid' }); + const getOrderFn = jest.spyOn(ordersRepo, 'getOrder').mockResolvedValue(order); + const transferToFn = jest.spyOn(casesRepo, 'createTransferTo'); + const transferFromFn = jest.spyOn(casesRepo, 'createTransferFrom'); + const auditFn = jest.spyOn(casesRepo, 'createCaseHistory'); + mockContext.session = await createMockApplicationContextSession({ user: unauthorizedUser }); + + await expect(useCase.updateTransferOrder(mockContext, order.id, action)).rejects.toThrow(); + expect(updateOrderFn).not.toHaveBeenCalled(); + expect(getOrderFn).not.toHaveBeenCalled(); + expect(transferToFn).not.toHaveBeenCalled(); + expect(transferFromFn).not.toHaveBeenCalled(); + expect(auditFn).not.toHaveBeenCalled(); + }); + + test('should throw an error if user is unauthorized to approve consolidations', async () => { + const pendingConsolidation = MockData.getConsolidationOrder(); + const leadCase = MockData.getCaseSummary(); + const approval: ConsolidationOrderActionApproval = { + ...pendingConsolidation, + approvedCases: pendingConsolidation.childCases.map((bCase) => { + return bCase.caseId; + }), + leadCase, + status: 'approved', + }; + const mockGetConsolidation = jest + .spyOn(casesRepo, 'getConsolidation') + .mockRejectedValue('We should never call this'); + mockContext.session = await createMockApplicationContextSession({ user: unauthorizedUser }); + + await expect(useCase.approveConsolidation(mockContext, approval)).rejects.toThrow( + 'Unauthorized', + ); + expect(mockGetConsolidation).not.toHaveBeenCalled(); + }); + + test('should throw an error if user is unauthorized to reject consolidations', async () => { + const pendingConsolidation = MockData.getConsolidationOrder(); + const leadCase = MockData.getCaseSummary(); + const rejection: ConsolidationOrderActionRejection = { + ...pendingConsolidation, + rejectedCases: pendingConsolidation.childCases.map((bCase) => { + return bCase.caseId; + }), + leadCase, + status: 'rejected', + }; + const mockDelete = jest + .spyOn(consolidationRepo, 'delete') + .mockRejectedValue('We should never call this'); + mockContext.session = await createMockApplicationContextSession({ user: unauthorizedUser }); + + await expect(useCase.rejectConsolidation(mockContext, rejection)).rejects.toThrow( + 'Unauthorized', + ); + expect(mockDelete).not.toHaveBeenCalled(); + }); + + test('test that approved orders identify the user who made the change', async () => { + const pendingConsolidation = MockData.getConsolidationOrder(); + const leadCase = MockData.getCaseSummary(); + const approval: ConsolidationOrderActionApproval = { + ...pendingConsolidation, + approvedCases: pendingConsolidation.childCases.map((bCase) => { + return bCase.caseId; + }), + leadCase, + status: 'approved', + }; + const mockCreateCaseHistory = jest.spyOn( + CasesCosmosDbRepository.prototype, + 'createCaseHistory', + ); + jest.spyOn(consolidationRepo, 'delete').mockResolvedValue({}); + jest + .spyOn(CaseAssignmentUseCase.prototype, 'createTrialAttorneyAssignments') + .mockImplementation(() => Promise.resolve()); + mockContext.session = await createMockApplicationContextSession({ user: authorizedUser }); + await useCase.approveConsolidation(mockContext, approval); + expect(mockCreateCaseHistory).toHaveBeenCalledWith( + mockContext, + expect.objectContaining({ updatedBy: getCamsUserReference(authorizedUser) }), + ); + }); + + test('test that rejected orders identify the user who made the change', async () => { + const pendingConsolidation = MockData.getConsolidationOrder(); + const leadCase = MockData.getCaseSummary(); + const rejection: ConsolidationOrderActionRejection = { + ...pendingConsolidation, + rejectedCases: pendingConsolidation.childCases.map((bCase) => { + return bCase.caseId; + }), + leadCase, + status: 'approved', + }; + const mockCreateCaseHistory = jest.spyOn( + CasesCosmosDbRepository.prototype, + 'createCaseHistory', + ); + jest.spyOn(consolidationRepo, 'delete').mockResolvedValue({}); + jest + .spyOn(CaseAssignmentUseCase.prototype, 'createTrialAttorneyAssignments') + .mockImplementation(() => Promise.resolve()); + mockContext.session = await createMockApplicationContextSession({ user: authorizedUser }); + await useCase.rejectConsolidation(mockContext, rejection); + expect(mockCreateCaseHistory).toHaveBeenCalledWith( + mockContext, + expect.objectContaining({ updatedBy: getCamsUserReference(authorizedUser) }), + ); + }); }); diff --git a/backend/functions/lib/use-cases/orders/orders.ts b/backend/functions/lib/use-cases/orders/orders.ts index e71a5d8c9a..dd69c143b1 100644 --- a/backend/functions/lib/use-cases/orders/orders.ts +++ b/backend/functions/lib/use-cases/orders/orders.ts @@ -34,6 +34,7 @@ import { CamsError } from '../../common-errors/cams-error'; import { sortDates, sortDatesReverse } from '../../../../../common/src/date-helper'; import * as crypto from 'crypto'; import { + CaseConsolidationHistory, CaseHistory, ConsolidationOrderSummary, isConsolidationHistory, @@ -42,6 +43,8 @@ import { CaseAssignmentUseCase } from '../case-assignment'; import { BadRequestError } from '../../common-errors/bad-request'; import { CamsUserReference } from '../../../../../common/src/cams/users'; import { CamsRole } from '../../../../../common/src/cams/roles'; +import { UnauthorizedError } from '../../common-errors/unauthorized-error'; +import { createAuditRecord } from '../../../../../common/src/cams/auditable'; const MODULE_NAME = 'ORDERS_USE_CASE'; export interface SyncOrdersOptions { @@ -82,8 +85,9 @@ export class OrdersUseCase { } public async getOrders(context: ApplicationContext): Promise> { - const transferOrders = await this.ordersRepo.getOrders(context); - const consolidationOrders = await this.consolidationsRepo.getAll(context); + const divisionCodes = context.session.user.offices.map((office) => office.courtDivisionCode); + const transferOrders = await this.ordersRepo.search(context, { divisionCodes }); + const consolidationOrders = await this.consolidationsRepo.search(context, { divisionCodes }); return transferOrders .concat(consolidationOrders) .sort((a, b) => sortDates(a.orderDate, b.orderDate)); @@ -99,6 +103,10 @@ export class OrdersUseCase { id: string, data: TransferOrderAction, ): Promise { + if (!context.session.user.roles.includes(CamsRole.DataVerifier)) { + throw new UnauthorizedError(MODULE_NAME); + } + context.logger.info(MODULE_NAME, 'Updating transfer order:', data); const initialOrder = await this.ordersRepo.getOrder(context, id, data.caseId); let order: Order; @@ -125,13 +133,15 @@ export class OrdersUseCase { await this.casesRepo.createTransferFrom(context, transferFrom); await this.casesRepo.createTransferTo(context, transferTo); } - const caseHistory: CaseHistory = { - caseId: order.caseId, - documentType: 'AUDIT_TRANSFER', - before: initialOrder as TransferOrder, - after: order, - occurredAtTimestamp: new Date().toISOString(), - }; + const caseHistory = createAuditRecord( + { + caseId: order.caseId, + documentType: 'AUDIT_TRANSFER', + before: initialOrder as TransferOrder, + after: order, + }, + context.session.user, + ); await this.casesRepo.createCaseHistory(context, caseHistory); } } @@ -190,13 +200,15 @@ export class OrdersUseCase { for (const order of writtenTransfers) { if (isTransferOrder(order)) { - const caseHistory: CaseHistory = { - caseId: order.caseId, - documentType: 'AUDIT_TRANSFER', - before: null, - after: order, - occurredAtTimestamp: new Date().toISOString(), - }; + const caseHistory = createAuditRecord( + { + caseId: order.caseId, + documentType: 'AUDIT_TRANSFER', + before: null, + after: order, + }, + context.session.user, + ); await this.casesRepo.createCaseHistory(context, caseHistory); } } @@ -214,13 +226,15 @@ export class OrdersUseCase { status: 'pending', childCases: [], }; - const caseHistory: CaseHistory = { - caseId: order.caseId, - documentType: 'AUDIT_CONSOLIDATION', - before: null, - after: history, - occurredAtTimestamp: new Date().toISOString(), - }; + const caseHistory = createAuditRecord( + { + caseId: order.caseId, + documentType: 'AUDIT_CONSOLIDATION', + before: null, + after: history, + }, + context.session.user, + ); await this.casesRepo.createCaseHistory(context, caseHistory); } @@ -279,7 +293,7 @@ export class OrdersUseCase { const fullHistory = await this.casesRepo.getCaseHistory(context, bCase.caseId); before = fullHistory .filter((h) => h.documentType === 'AUDIT_CONSOLIDATION') - .sort((a, b) => sortDatesReverse(a.occurredAtTimestamp, b.occurredAtTimestamp)) + .sort((a, b) => sortDatesReverse(a.updatedOn, b.updatedOn)) .shift()?.after; } catch { before = undefined; @@ -288,13 +302,15 @@ export class OrdersUseCase { if (isConsolidationHistory(before) && before.childCases.length > 0) { after.childCases.push(...before.childCases); } - return { - caseId: bCase.caseId, - documentType: 'AUDIT_CONSOLIDATION', - before: isConsolidationHistory(before) ? before : null, - after, - occurredAtTimestamp: new Date().toISOString(), - }; + return createAuditRecord( + { + caseId: bCase.caseId, + documentType: 'AUDIT_CONSOLIDATION', + before: isConsolidationHistory(before) ? before : null, + after, + }, + context.session.user, + ); } private async handleConsolidation( @@ -305,6 +321,10 @@ export class OrdersUseCase { consolidationType?: ConsolidationType, leadCase?: CaseSummary, ): Promise { + if (!context.session.user.roles.includes(CamsRole.DataVerifier)) { + throw new UnauthorizedError(MODULE_NAME); + } + const includedChildCases = provisionalOrder.childCases.filter((c) => includedCases.includes(c.caseId), ); diff --git a/backend/functions/orders-manual-sync/orders-manual-sync.function.test.ts b/backend/functions/orders-manual-sync/orders-manual-sync.function.test.ts index f925e58f2b..604496ee7c 100644 --- a/backend/functions/orders-manual-sync/orders-manual-sync.function.test.ts +++ b/backend/functions/orders-manual-sync/orders-manual-sync.function.test.ts @@ -31,7 +31,7 @@ describe('Orders Sync Function tests', () => { test('Should call orders controller method syncOrders', async () => { const { camsHttpResponse } = buildTestResponseSuccess({ data: syncResponse }); const request = createMockAzureFunctionRequest({ - url: 'http://domain/api/order-manual-sync', + url: 'http://domain/api/orders-sync', params: {}, method: 'POST', }); @@ -44,7 +44,7 @@ describe('Orders Sync Function tests', () => { test('Should log a camsError if syncOrders throws a CamsError', async () => { const request = createMockAzureFunctionRequest({ - url: 'http://domain/api/order-manual-sync', + url: 'http://domain/api/orders-sync', params: {}, method: 'POST', }); diff --git a/common/src/api/search.ts b/common/src/api/search.ts index 44ea63972c..105026a718 100644 --- a/common/src/api/search.ts +++ b/common/src/api/search.ts @@ -23,3 +23,7 @@ export type CasesSearchPredicate = SearchPredicate & { assignments?: CamsUserReference[]; caseIds?: string[]; }; + +export type OrdersSearchPredicate = SearchPredicate & { + divisionCodes?: string[]; +}; diff --git a/common/src/cams/assignments.ts b/common/src/cams/assignments.ts index e0e9c34dc4..cad24df1c9 100644 --- a/common/src/cams/assignments.ts +++ b/common/src/cams/assignments.ts @@ -1,7 +1,8 @@ import { CamsUserReference } from './users'; import { CamsRole } from './roles'; +import { Auditable } from './auditable'; -export type CaseAssignment = { +export type CaseAssignment = Auditable & { id?: string; documentType: 'ASSIGNMENT'; caseId: string; @@ -10,7 +11,6 @@ export type CaseAssignment = { role: string; assignedOn: string; unassignedOn?: string; - changedBy: CamsUserReference; }; export type StaffAssignmentAction = { diff --git a/common/src/cams/auditable.ts b/common/src/cams/auditable.ts new file mode 100644 index 0000000000..dd75260ef8 --- /dev/null +++ b/common/src/cams/auditable.ts @@ -0,0 +1,21 @@ +import { getCamsUserReference } from './session'; +import { CamsUserReference } from './users'; + +export type Auditable = { + updatedOn: string; + updatedBy: CamsUserReference; +}; + +export const SYSTEM_USER_REFERENCE: CamsUserReference = { id: 'SYSTEM', name: 'SYSTEM' }; + +export function createAuditRecord< + T extends Auditable, + U extends CamsUserReference = CamsUserReference, +>(record: Omit, user?: U, override?: Partial): T { + return { + updatedOn: new Date().toISOString(), + updatedBy: user ? getCamsUserReference(user) : SYSTEM_USER_REFERENCE, + ...record, + ...override, + } as T; +} diff --git a/common/src/cams/history.ts b/common/src/cams/history.ts index 68f552f410..f687b3f299 100644 --- a/common/src/cams/history.ts +++ b/common/src/cams/history.ts @@ -1,7 +1,7 @@ import { CaseAssignment } from './assignments'; +import { Auditable } from './auditable'; import { CaseSummary } from './cases'; import { OrderStatus, TransferOrder } from './orders'; -import { CamsUserReference } from './users'; export interface ConsolidationOrderSummary { status: OrderStatus; @@ -19,14 +19,11 @@ export function isConsolidationHistory(history: unknown): history is Consolidati return typeof history === 'object' && 'status' in history; } -// TODO: Consider a way to make the occurredAtTimestamp optional when creating a record, otherwise it is required. -type AbstractCaseHistory = { +type AbstractCaseHistory = Auditable & { id?: string; caseId: string; - occurredAtTimestamp: string; before: B; after: A; - changedBy?: CamsUserReference; }; export type CaseAssignmentHistory = AbstractCaseHistory & { diff --git a/common/src/cams/roles.ts b/common/src/cams/roles.ts index d1da00733a..91659860a2 100644 --- a/common/src/cams/roles.ts +++ b/common/src/cams/roles.ts @@ -2,4 +2,5 @@ export enum CamsRole { SuperUser = 'SuperUser', CaseAssignmentManager = 'CaseAssignmentManager', TrialAttorney = 'TrialAttorney', + DataVerifier = 'DataVerifier', } diff --git a/common/src/cams/test-utilities/mock-data.ts b/common/src/cams/test-utilities/mock-data.ts index 4a9ff514ce..a585db3d0a 100644 --- a/common/src/cams/test-utilities/mock-data.ts +++ b/common/src/cams/test-utilities/mock-data.ts @@ -99,6 +99,13 @@ function someDateAfterThisDate(thisDateString: string, days?: number): string { return someDate.toISOString().split('T')[0]; } +function someDateBeforeThisDate(thisDateString: string, days?: number): string { + const thisDate = new Date(Date.parse(thisDateString)); + const daysToSubtract = days || randomInt(1000); + const someDate = new Date(thisDate.setDate(thisDate.getDate() - daysToSubtract)); + return someDate.toISOString().split('T')[0]; +} + function randomChapter(chapters: BankruptcyChapters[] = ['9', '11', '12', '15']) { return chapters[randomInt(chapters.length - 1)]; } @@ -257,7 +264,10 @@ function getTransferOrder(options: Options = { override: {} }): T ...summary, id: faker.string.uuid(), orderType: 'transfer', - orderDate: someDateAfterThisDate(summary.dateFiled), + orderDate: override.orderDate ?? someDateAfterThisDate(summary.dateFiled), + dateFiled: + override.dateFiled ?? + (override.orderDate ? someDateBeforeThisDate(override.orderDate) : summary.dateFiled), status: override.status || 'pending', docketEntries: [getDocketEntry()], docketSuggestedCaseNumber: override.status === 'approved' ? undefined : randomCaseNumber(), @@ -280,7 +290,7 @@ function getConsolidationOrder( courtName: summary.courtName, id: faker.string.uuid(), orderType: 'consolidation', - orderDate: someDateAfterThisDate(summary.dateFiled), + orderDate: override.orderDate ?? someDateAfterThisDate(summary.dateFiled), status: override.status || 'pending', courtDivisionCode: summary.courtDivisionCode, jobId: faker.number.int(), @@ -402,6 +412,7 @@ function getDebtorAttorney(override: Partial = {}): DebtorAttorn function getAttorneyAssignment(override: Partial = {}): CaseAssignment { const firstDate = someDateAfterThisDate(`2023-01-01`, 28); + const secondDate = someDateAfterThisDate(firstDate, 28); return { id: randomId(), documentType: 'ASSIGNMENT', @@ -410,8 +421,9 @@ function getAttorneyAssignment(override: Partial = {}): CaseAssi name: faker.person.fullName(), role: 'TrialAttorney', assignedOn: firstDate, - unassignedOn: someDateAfterThisDate(firstDate, 28), - changedBy: getCamsUserReference(), + unassignedOn: secondDate, + updatedOn: secondDate, + updatedBy: getCamsUserReference(), ...override, }; } @@ -466,12 +478,18 @@ function getAttorneyUser(override: Partial = {}): AttorneyUser { } function getCamsSession(override: Partial = {}): CamsSession { + let offices = [MANHATTAN]; + let roles = []; + if (override?.user?.roles.includes(CamsRole.SuperUser)) { + offices = OFFICES; + roles = Object.values(CamsRole); + } return { user: { id: randomId(), name: 'Mock Name', - offices: [MANHATTAN], - roles: [], + offices, + roles, }, accessToken: getJwt(), provider: 'mock', diff --git a/common/src/cams/test-utilities/mock-user.ts b/common/src/cams/test-utilities/mock-user.ts index 122b59b38f..141b6084fe 100644 --- a/common/src/cams/test-utilities/mock-user.ts +++ b/common/src/cams/test-utilities/mock-user.ts @@ -1,7 +1,7 @@ import { OfficeDetails } from '../courts'; import { CamsRole } from '../roles'; import { CamsUser } from '../users'; -import { BUFFALO, DELAWARE, MANHATTAN, WHITE_PLAINS } from './offices.mock'; +import { BUFFALO, DELAWARE, MANHATTAN, OFFICES, WHITE_PLAINS } from './offices.mock'; const REGION_02_GROUP_NY: OfficeDetails[] = [MANHATTAN, WHITE_PLAINS]; const REGION_02_GROUP_BU: OfficeDetails[] = [BUFFALO]; @@ -14,18 +14,27 @@ export type MockUser = { hide?: boolean; }; +function addSuperUserOffices(user: CamsUser) { + if (user.roles.includes(CamsRole.SuperUser)) { + user.offices = OFFICES; + user.roles = Object.values(CamsRole); + } +} + export const SUPERUSER = { sub: 'user@fake.com', label: "Martha's Son - Super User", user: { id: '==MOCKUSER=user@fake.com==', name: "Martha's Son", - roles: [CamsRole.SuperUser, CamsRole.CaseAssignmentManager, CamsRole.TrialAttorney], + roles: [CamsRole.SuperUser], offices: [], }, hide: true, }; +addSuperUserOffices(SUPERUSER.user); + export const MockUsers: MockUser[] = [ { sub: 'jpearson@fake.com', @@ -68,15 +77,25 @@ export const MockUsers: MockUser[] = [ }, }, { - sub: 'paralegal', - label: 'Bert - Paralegal', + sub: 'bert@fake.com', + label: 'Bert - Data Verifier (Manhattan)', user: { - id: '==MOCKUSER=paralegal==', + id: 'bert@fake.com', name: 'Bert', - roles: [], + roles: [CamsRole.DataVerifier], offices: REGION_02_GROUP_NY, }, }, + { + sub: 'earnie@fake.com', + label: 'Earnie - Data Verifier (Buffalo)', + user: { + id: 'earnie@fake.com', + name: 'Earnie', + roles: [CamsRole.DataVerifier], + offices: REGION_02_GROUP_BU, + }, + }, { sub: 'charlie@fake.com', label: 'Charlie - Assistant US Trustee (Manhattan)', diff --git a/user-interface/src/case-detail/panels/CaseDetailAuditHistory.test.tsx b/user-interface/src/case-detail/panels/CaseDetailAuditHistory.test.tsx index 72d7cfd09d..22b4425e68 100644 --- a/user-interface/src/case-detail/panels/CaseDetailAuditHistory.test.tsx +++ b/user-interface/src/case-detail/panels/CaseDetailAuditHistory.test.tsx @@ -5,6 +5,7 @@ import { CaseHistory } from '@common/cams/history'; import { ConsolidationOrder } from '@common/cams/orders'; import MockData from '@common/cams/test-utilities/mock-data'; import Api2 from '@/lib/models/api2'; +import { SYSTEM_USER_REFERENCE } from '@common/cams/auditable'; describe('audit history tests', () => { const caseId = '000-11-22222'; @@ -28,7 +29,8 @@ describe('audit history tests', () => { name: 'Alfred', role: 'TrialAttorney', assignedOn: '2023-12-25T00:00:00.000Z', - changedBy: MockData.getCamsUserReference(), + updatedOn: '2023-12-25T00:00:00.000Z', + updatedBy: MockData.getCamsUserReference(), }, { caseId, @@ -37,7 +39,8 @@ describe('audit history tests', () => { name: 'Bradford', role: 'TrialAttorney', assignedOn: '2023-12-25T00:00:00.000Z', - changedBy: MockData.getCamsUserReference(), + updatedOn: '2023-12-25T00:00:00.000Z', + updatedBy: MockData.getCamsUserReference(), }, ]; const assignmentAfter: CaseAssignment[] = [ @@ -48,7 +51,8 @@ describe('audit history tests', () => { name: 'Charles', role: 'TrialAttorney', assignedOn: '2023-12-25T00:00:00.000Z', - changedBy: MockData.getCamsUserReference(), + updatedOn: '2023-12-25T00:00:00.000Z', + updatedBy: MockData.getCamsUserReference(), }, { caseId, @@ -57,7 +61,8 @@ describe('audit history tests', () => { name: 'Daniel', role: 'TrialAttorney', assignedOn: '2023-12-25T00:00:00.000Z', - changedBy: MockData.getCamsUserReference(), + updatedOn: '2023-12-25T00:00:00.000Z', + updatedBy: MockData.getCamsUserReference(), }, { caseId, @@ -66,7 +71,8 @@ describe('audit history tests', () => { name: 'Edward', role: 'TrialAttorney', assignedOn: '2023-12-25T00:00:00.000Z', - changedBy: MockData.getCamsUserReference(), + updatedOn: '2023-12-25T00:00:00.000Z', + updatedBy: MockData.getCamsUserReference(), }, ]; const caseHistory: CaseHistory[] = [ @@ -74,37 +80,37 @@ describe('audit history tests', () => { id: '1234567890', documentType: 'AUDIT_ASSIGNMENT', caseId, - occurredAtTimestamp: '2023-12-25T00:00:00.000Z', + updatedOn: '2023-12-25T00:00:00.000Z', before: assignmentBefore, after: assignmentAfter, - changedBy: MockData.getCamsUserReference(), + updatedBy: MockData.getCamsUserReference(), }, { id: '1234567890', documentType: 'AUDIT_TRANSFER', caseId, - occurredAtTimestamp: '2023-12-25T00:00:00.000Z', + updatedOn: '2023-12-25T00:00:00.000Z', before: pendingTransferOrder, after: approvedTransferOrder, - changedBy: MockData.getCamsUserReference(), + updatedBy: MockData.getCamsUserReference(), }, { id: '1234567890', documentType: 'AUDIT_CONSOLIDATION', caseId, - occurredAtTimestamp: '2023-12-25T00:00:00.000Z', + updatedOn: '2023-12-25T00:00:00.000Z', before: null, after: pendingConsolidationOrder, - changedBy: MockData.getCamsUserReference(), + updatedBy: MockData.getCamsUserReference(), }, { id: '1234567890', documentType: 'AUDIT_CONSOLIDATION', caseId, - occurredAtTimestamp: '2023-12-25T00:00:00.000Z', + updatedOn: '2023-12-25T00:00:00.000Z', before: pendingConsolidationOrder, after: rejectedConsolidationOrder, - changedBy: MockData.getCamsUserReference(), + updatedBy: MockData.getCamsUserReference(), }, ]; @@ -157,9 +163,9 @@ describe('audit history tests', () => { expect(dateElement).toBeInTheDocument(); expect(dateElement).toHaveTextContent('12/25/2023'); - const changedByElement = screen.queryByTestId('changed-by-0'); - expect(changedByElement).toBeInTheDocument(); - expect(changedByElement).toHaveTextContent(caseHistory[0].changedBy!.name); + const updatedByElement = screen.queryByTestId('changed-by-0'); + expect(updatedByElement).toBeInTheDocument(); + expect(updatedByElement).toHaveTextContent(caseHistory[0].updatedBy!.name); }); test('should display (none) when no assignments exist.', async () => { @@ -168,7 +174,8 @@ describe('audit history tests', () => { id: '', documentType: 'AUDIT_ASSIGNMENT', caseId, - occurredAtTimestamp: '', + updatedOn: '', + updatedBy: SYSTEM_USER_REFERENCE, before: [], after: [], }, @@ -196,7 +203,8 @@ describe('audit history tests', () => { id: '', documentType: 'AUDIT_TRANSFER', caseId, - occurredAtTimestamp: '2024-01-31T12:00:00Z', + updatedOn: '2024-01-31T12:00:00Z', + updatedBy: SYSTEM_USER_REFERENCE, before: pendingTransferOrder, after: approvedTransferOrder, }, @@ -204,7 +212,8 @@ describe('audit history tests', () => { id: '', documentType: 'AUDIT_TRANSFER', caseId, - occurredAtTimestamp: '2024-01-29T12:00:00Z', + updatedOn: '2024-01-29T12:00:00Z', + updatedBy: SYSTEM_USER_REFERENCE, before: null, after: pendingTransferOrder, }, diff --git a/user-interface/src/case-detail/panels/CaseDetailAuditHistory.tsx b/user-interface/src/case-detail/panels/CaseDetailAuditHistory.tsx index 13427bca4d..b479594e39 100644 --- a/user-interface/src/case-detail/panels/CaseDetailAuditHistory.tsx +++ b/user-interface/src/case-detail/panels/CaseDetailAuditHistory.tsx @@ -58,10 +58,10 @@ export default function CaseDetailAuditHistory(props: CaseDetailAuditHistoryProp .join(', ')} - {history.changedBy && <>{history.changedBy.name}} + {history.updatedBy && <>{history.updatedBy.name}} - {formatDate(history.occurredAtTimestamp)} + {formatDate(history.updatedOn)} ); @@ -82,10 +82,10 @@ export default function CaseDetailAuditHistory(props: CaseDetailAuditHistoryProp {history.after && orderStatusType.get(history.after.status)} - {history.changedBy && <>{history.changedBy.name}} + {history.updatedBy && <>{history.updatedBy.name}} - {formatDate(history.occurredAtTimestamp)} + {formatDate(history.updatedOn)} ); diff --git a/user-interface/src/data-verification/DataVerificationScreen.test.tsx b/user-interface/src/data-verification/DataVerificationScreen.test.tsx index b12ef38cbd..2f8d3808e0 100644 --- a/user-interface/src/data-verification/DataVerificationScreen.test.tsx +++ b/user-interface/src/data-verification/DataVerificationScreen.test.tsx @@ -12,9 +12,15 @@ import { OfficeDetails } from '@common/cams/courts'; import * as FeatureFlagHook from '@/lib/hooks/UseFeatureFlags'; import Api2 from '@/lib/models/api2'; import MockData from '@common/cams/test-utilities/mock-data'; +import testingUtilities from '@/lib/testing/testing-utilities'; +import { CamsRole } from '@common/cams/roles'; +import LocalStorage from '@/lib/utils/local-storage'; describe('Review Orders screen', () => { + const user = testingUtilities.setUserWithRoles([CamsRole.DataVerifier]); + beforeEach(async () => { + LocalStorage.setSession(MockData.getCamsSession({ user })); vi.stubEnv('CAMS_PA11Y', 'true'); }); @@ -256,6 +262,20 @@ describe('Review Orders screen', () => { expect(consolidationsFilter).not.toBeInTheDocument(); }); + test('should render permission invalid error when CaseAssignmentManager is not found in user roles', async () => { + testingUtilities.setUserWithRoles([]); + const unauthorizedUser = MockData.getCamsUser({ roles: [CamsRole.CaseAssignmentManager] }); + LocalStorage.setSession(MockData.getCamsSession({ user: unauthorizedUser })); + const alertSpy = testingUtilities.spyOnGlobalAlert(); + render( + + + , + ); + + expect(alertSpy.error).toHaveBeenCalledWith('Invalid Permissions'); + }); + test('should not render a list if an API error is encountered', async () => { const mock = vi.spyOn(Api2, 'getOrders'); mock.mockRejectedValue({}); diff --git a/user-interface/src/data-verification/DataVerificationScreen.tsx b/user-interface/src/data-verification/DataVerificationScreen.tsx index 3ae5e08d50..4c70e73ea6 100644 --- a/user-interface/src/data-verification/DataVerificationScreen.tsx +++ b/user-interface/src/data-verification/DataVerificationScreen.tsx @@ -23,6 +23,9 @@ import { useApi2 } from '@/lib/hooks/UseApi2'; import DocumentTitle from '@/lib/components/cams/DocumentTitle/DocumentTitle'; import { MainContent } from '@/lib/components/cams/MainContent/MainContent'; import { ResponseBody } from '@common/api/response'; +import { CamsRole } from '@common/cams/roles'; +import LocalStorage from '@/lib/utils/local-storage'; +import { useGlobalAlert } from '@/lib/hooks/UseGlobalAlert'; export function officeSorter(a: OfficeDetails, b: OfficeDetails) { const aKey = a.courtName + '-' + a.courtDivisionName; @@ -46,10 +49,17 @@ export default function DataVerificationScreen() { timeOut: 8, }); + const globalAlert = useGlobalAlert(); + const session = LocalStorage.getSession(); const regionNumber = '02'; const api = useApi2(); + if (!session?.user?.roles?.includes(CamsRole.DataVerifier)) { + globalAlert?.error('Invalid Permissions'); + return <>; + } + async function getOrders() { setIsOrderListLoading(true); api diff --git a/user-interface/src/data-verification/DataVerificationScreenAlert.test.tsx b/user-interface/src/data-verification/DataVerificationScreenAlert.test.tsx index 573b4cbe31..90440e3f15 100644 --- a/user-interface/src/data-verification/DataVerificationScreenAlert.test.tsx +++ b/user-interface/src/data-verification/DataVerificationScreenAlert.test.tsx @@ -4,9 +4,15 @@ import { UswdsAlertStyle } from '@/lib/components/uswds/Alert'; import { BrowserRouter } from 'react-router-dom'; import DataVerificationScreen from './DataVerificationScreen'; import { MockData } from '@common/cams/test-utilities/mock-data'; +import testingUtilities from '@/lib/testing/testing-utilities'; +import { CamsRole } from '@common/cams/roles'; +import LocalStorage from '@/lib/utils/local-storage'; describe('Review Orders screen - Alert', () => { + const user = testingUtilities.setUserWithRoles([CamsRole.DataVerifier]); + beforeEach(async () => { + LocalStorage.setSession(MockData.getCamsSession({ user })); vi.stubEnv('CAMS_PA11Y', 'true'); }); diff --git a/user-interface/src/data-verification/transfer/PendingTransferOrder.test.tsx b/user-interface/src/data-verification/transfer/PendingTransferOrder.test.tsx index 04c156cf4b..258237d76b 100644 --- a/user-interface/src/data-verification/transfer/PendingTransferOrder.test.tsx +++ b/user-interface/src/data-verification/transfer/PendingTransferOrder.test.tsx @@ -230,13 +230,8 @@ describe('PendingTransferOrder component', () => { }); test('should display modal and when Approve is clicked, upon submission of modal should update the status of order to approved', async () => { - const patchSpy = vi.spyOn(Api2, 'patchTransferOrder').mockResolvedValue({ - data: MockData.getTransferOrder({ - override: { - dateFiled: order.dateFiled, - debtor: order.debtor, - }, - }), + const patchSpy = vi.spyOn(Api2, 'patchTransferOrder').mockImplementation(() => { + return Promise.resolve(); }); vi.spyOn(Api2, 'getCaseSummary') .mockResolvedValueOnce(mockGetCaseSummary) @@ -288,13 +283,8 @@ describe('PendingTransferOrder component', () => { }); test('should properly reject when API returns a successful response and a reason is supplied', async () => { - const patchSpy = vi.spyOn(Api2, 'patchTransferOrder').mockResolvedValue({ - data: MockData.getTransferOrder({ - override: { - dateFiled: order.dateFiled, - debtor: order.debtor, - }, - }), + const patchSpy = vi.spyOn(Api2, 'patchTransferOrder').mockImplementation(() => { + return Promise.resolve(); }); vi.spyOn(Api2, 'getCaseSummary') .mockResolvedValueOnce(mockGetCaseSummary) diff --git a/user-interface/src/lib/components/Header.test.tsx b/user-interface/src/lib/components/Header.test.tsx index 28b5e16dbc..ba3979e870 100644 --- a/user-interface/src/lib/components/Header.test.tsx +++ b/user-interface/src/lib/components/Header.test.tsx @@ -8,13 +8,18 @@ import MockData from '@common/cams/test-utilities/mock-data'; import { CamsRole } from '@common/cams/roles'; describe('Header', () => { - const user = MockData.getCamsUser({ roles: [CamsRole.CaseAssignmentManager] }); - LocalStorage.setSession(MockData.getCamsSession({ user })); + const user = MockData.getCamsUser({ + roles: [CamsRole.CaseAssignmentManager, CamsRole.DataVerifier], + }); vi.spyOn(FeatureFlags, 'default').mockReturnValue({ 'transfer-orders-enabled': true, 'case-search-enabled': true, }); + beforeEach(() => { + LocalStorage.setSession(MockData.getCamsSession({ user })); + }); + function renderWithoutProps() { render( @@ -91,4 +96,20 @@ describe('Header', () => { const current = document.querySelectorAll('.usa-current.current'); expect(current).toHaveLength(1); }); + + test('should not display data verification link when unauthorized', () => { + const unauthorizedUser = MockData.getCamsUser({ roles: [CamsRole.CaseAssignmentManager] }); + LocalStorage.setSession(MockData.getCamsSession({ user: unauthorizedUser })); + renderWithoutProps(); + + const link = screen.queryByTestId('header-data-verification-link'); + expect(link).not.toBeInTheDocument(); + }); + + test('should display data verification link when authorized', async () => { + renderWithoutProps(); + + const link = screen.queryByTestId('header-data-verification-link'); + expect(link).toBeInTheDocument(); + }); }); diff --git a/user-interface/src/lib/components/Header.tsx b/user-interface/src/lib/components/Header.tsx index 209ea8806c..26a9df8eb1 100644 --- a/user-interface/src/lib/components/Header.tsx +++ b/user-interface/src/lib/components/Header.tsx @@ -112,23 +112,25 @@ export const Header = () => { )} - {transferOrdersFlag && ( -
  • - { - return setActiveNav(NavState.DATA_VERIFICATION); - }} - title="view status of, approve, or reject case events" - > - Data Verification - -
  • - )} + {session && + session.user.roles?.includes(CamsRole.DataVerifier) && + transferOrdersFlag && ( +
  • + { + return setActiveNav(NavState.DATA_VERIFICATION); + }} + title="view status of, approve, or reject case events" + > + Data Verification + +
  • + )} {caseSearchFlag && (
  • diff --git a/user-interface/src/lib/models/api.test.ts b/user-interface/src/lib/models/api.test.ts index 015e6cbe0d..d72c89401d 100644 --- a/user-interface/src/lib/models/api.test.ts +++ b/user-interface/src/lib/models/api.test.ts @@ -4,6 +4,10 @@ import * as httpAdapter from '../utils/http.adapter'; import { vi } from 'vitest'; describe('Specific tests for the API model', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + test('createPath should return a properly constructed URL when passed a basic path and an array of query parameters', () => { const path = '/foo/bar'; const params: ObjectKeyVal = { @@ -138,6 +142,7 @@ describe('Specific tests for the API model', () => { const payload = { foo: 'mock patch' }; const mockHttpPatch = vi.fn().mockResolvedValue({ json: () => Promise.resolve(payload), + text: () => Promise.resolve(JSON.stringify(payload)), status: 200, ok: true, }); diff --git a/user-interface/src/lib/models/api.ts b/user-interface/src/lib/models/api.ts index 59f602667f..a550280d24 100644 --- a/user-interface/src/lib/models/api.ts +++ b/user-interface/src/lib/models/api.ts @@ -109,11 +109,20 @@ export default class Api { } } + /** + * ONLY USE WITH OUR OWN API!!!! + * This function makes assumptions about the responses to PATCH requests that do not handle + * all possibilities according to the HTTP specifications. + * + * @param path string The path after '/api'. + * @param body object The payload for the request. + * @param options ObjectKeyVal Query params in the form of key/value pairs. + */ public static async patch( path: string, body: object, options?: ObjectKeyVal, - ): Promise { + ): Promise { try { await this.executeBeforeHooks(); const apiOptions = this.getQueryStringsToPassThrough(window.location.search, options); @@ -121,12 +130,12 @@ export default class Api { const response = await httpPatch({ url: Api.host + pathStr, body, headers: this.headers }); await this.executeAfterHooks(response); - const data = await response.json(); - if (response.ok) { - return data; + const data = await response.text(); + return data.length > 1 ? JSON.parse(data) : undefined; } else { - return Promise.reject(new Error(data.message)); + const error = await response.json(); + return Promise.reject(new Error(error.message)); } } catch (e: unknown) { return Promise.reject(new Error(`500 Error - Server Error ${(e as Error).message}`)); diff --git a/user-interface/src/lib/models/api2.test.ts b/user-interface/src/lib/models/api2.test.ts index 942fb1d682..53b35b7781 100644 --- a/user-interface/src/lib/models/api2.test.ts +++ b/user-interface/src/lib/models/api2.test.ts @@ -5,6 +5,8 @@ import Api, { addApiAfterHook, addApiBeforeHook } from '@/lib/models/api'; import MockData from '@common/cams/test-utilities/mock-data'; import { StaffAssignmentAction } from '@common/cams/assignments'; import { CamsRole } from '@common/cams/roles'; +import { randomUUID } from 'crypto'; +import { TransferOrderAction } from '@common/cams/orders'; type ApiType = { addApiBeforeHook: typeof addApiBeforeHook; @@ -96,7 +98,6 @@ describe('_Api2 functions', async () => { await callApiFunction(api2.Api2.getOffices, null, api); await callApiFunction(api2.Api2.getOrders, null, api); await callApiFunction(api2.Api2.getOrderSuggestions, 'some-id', api); - await callApiFunction(api2.Api2.patchTransferOrder, 'some-id', api); await callApiFunction(api2.Api2.putConsolidationOrderApproval, 'some-id', api); await callApiFunction(api2.Api2.putConsolidationOrderRejection, 'some-id', api); await callApiFunction(api2.Api2.searchCases, 'some-id', api); @@ -113,6 +114,19 @@ describe('_Api2 functions', async () => { }; await api2.Api2.postStaffAssignments(assignmentAction); expect(postSpy).toHaveBeenCalled(); + + const patchSpy = vi.spyOn(api.default, 'patch').mockImplementation(() => { + return Promise.resolve(); + }); + const approval: TransferOrderAction = { + id: randomUUID(), + caseId: MockData.randomCaseId(), + orderType: 'transfer', + newCase: MockData.getCaseSummary(), + status: 'approved', + }; + await api2.Api2.patchTransferOrder(approval); + expect(patchSpy).toHaveBeenCalled(); }); test('should handle error properly', async () => { diff --git a/user-interface/src/lib/models/api2.ts b/user-interface/src/lib/models/api2.ts index 8244b35600..d958a5017a 100644 --- a/user-interface/src/lib/models/api2.ts +++ b/user-interface/src/lib/models/api2.ts @@ -27,14 +27,28 @@ interface ApiClient { post(path: string, body: object, options?: ObjectKeyVal): Promise; get(path: string, options?: ObjectKeyVal): Promise; - patch(path: string, body: object, options?: ObjectKeyVal): Promise; + patch(path: string, body: object, options?: ObjectKeyVal): Promise; put(path: string, body: object, options?: ObjectKeyVal): Promise; getQueryStringsToPassThrough(search: string, options: ObjectKeyVal): ObjectKeyVal; } interface GenericApiClient { get(path: string, options?: ObjectKeyVal): Promise>; - patch(path: string, body: object, options?: ObjectKeyVal): Promise>; + + /** + * ONLY USE WITH OUR OWN API!!!! + * This function makes assumptions about the responses to PATCH requests that do not handle + * all possibilities according to the HTTP specifications. + * + * @param path string The path after '/api'. + * @param body object The payload for the request. + * @param options ObjectKeyVal Query params in the form of key/value pairs. + */ + patch( + path: string, + body: object, + options?: ObjectKeyVal, + ): Promise | void>; /** * ONLY USE WITH OUR OWN API!!!! @@ -90,8 +104,11 @@ export function useGenericApi(): GenericApiClient { path: string, body: object, options?: ObjectKeyVal, - ): Promise> { + ): Promise | void> { const responseBody = await api.patch(justThePath(path), body, options); + if (!responseBody) { + return; + } return responseBody as ResponseBody; }, async post( @@ -162,7 +179,7 @@ async function getOrderSuggestions(caseId: string) { } async function patchTransferOrder(data: FlexibleTransferOrderAction) { - return api().patch(`/orders/${data.id}`, data); + await api().patch(`/orders/${data.id}`, data); } async function putConsolidationOrderApproval(data: ConsolidationOrderActionApproval) { diff --git a/user-interface/src/lib/testing/mock-api2.ts b/user-interface/src/lib/testing/mock-api2.ts index 0009483201..89834cce7d 100644 --- a/user-interface/src/lib/testing/mock-api2.ts +++ b/user-interface/src/lib/testing/mock-api2.ts @@ -16,7 +16,6 @@ import { ConsolidationOrderActionRejection, FlexibleTransferOrderAction, Order, - TransferOrder, } from '@common/cams/orders'; import { CasesSearchPredicate } from '@common/api/search'; @@ -177,7 +176,7 @@ async function get(path: string): Promise> { return Promise.resolve(response as ResponseBody); } -async function patch( +async function _patch( _path: string, data: object, _options?: ObjectKeyVal, @@ -243,10 +242,8 @@ async function getOrderSuggestions(caseId: string): Promise(`/orders-suggestions/${caseId}/`); } -async function patchTransferOrder( - data: FlexibleTransferOrderAction, -): Promise> { - return patch(`/orders/${data.id}`, data); +async function patchTransferOrder(_data: FlexibleTransferOrderAction): Promise { + return Promise.resolve(); } async function putConsolidationOrderApproval(