Skip to content

Commit

Permalink
Merge pull request #1029 from US-Trustee-Program/CAMS-473-tag-divisions
Browse files Browse the repository at this point in the history
CAMS-473 - Identify legacy and invalid divisions and filter where necessary
  • Loading branch information
btposey authored Nov 13, 2024
2 parents 0740a66 + dbf1de0 commit 5cb3d35
Show file tree
Hide file tree
Showing 18 changed files with 337 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import { buildOfficeCode, getOfficeName } from '../../../use-cases/offices/offic

const MODULE_NAME = 'OFFICES-GATEWAY';

// Remove invalid divisions at the gateway rather than forcing the
// more important use case code to include logic to remove them.
const INVALID_DIVISION_CODES = ['070', '990', '991', '992', '993', '994', '995', '996', '999'];
const INVALID_DIVISION_CODES_SQL = INVALID_DIVISION_CODES.map((code) => "'" + code + "'").join(',');

type DxtrFlatOfficeDetails = {
officeName: string;
officeCode: string;
Expand Down Expand Up @@ -82,7 +87,6 @@ export default class OfficesDxtrGateway implements OfficesGateway {

async getOffices(context: ApplicationContext): Promise<UstpOfficeDetails[]> {
const input: DbTableFieldSpec[] = [];

const query = `
SELECT a.[CS_DIV] AS courtDivisionCode
,a.[GRP_DES] AS groupDesignator
Expand All @@ -98,6 +102,7 @@ export default class OfficesDxtrGateway implements OfficesGateway {
JOIN [dbo].[AO_COURT] c on a.COURT_ID = c.COURT_ID
JOIN [dbo].[AO_GRP_DES] d on a.GRP_DES = d.GRP_DES
JOIN [dbo].[AO_REGION] r on d.REGION_ID = r.REGION_ID
WHERE a.[CS_DIV] not in (${INVALID_DIVISION_CODES_SQL})
ORDER BY a.GRP_DES, a.OFFICE_CODE`;

const queryResult: QueryResults = await executeQuery(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { CamsRole } from '../../../../../../common/src/cams/roles';
import { StorageGateway } from '../../types/storage';
import { USTP_OFFICES_ARRAY, UstpOfficeDetails } from '../../../../../../common/src/cams/offices';

let roleMapping;
import {
USTP_OFFICES_ARRAY,
UstpDivisionMeta,
UstpOfficeDetails,
} from '../../../../../../common/src/cams/offices';

let roleMapping: Map<string, CamsRole>;
export const ROLE_MAPPING_PATH = '/rolemapping.csv';
const ROLE_MAPPING =
'ad_group_name,idp_group_name,cams_role\n' +
Expand All @@ -24,6 +27,9 @@ const OFFICE_MAPPING =
'USTP_CAMS_Region_2_Office_New_Haven,USTP CAMS Region 2 Office New Haven,NH\n' +
'USTP_CAMS_Region_18_Office_Seattle,USTP CAMS Region 18 Office Seattle,SE|AK\n';

let metaMapping: Map<string, UstpDivisionMeta>;
const LEGACY_DIVISION_CODES = [];

const storage = new Map<string, string>();
storage.set(ROLE_MAPPING_PATH, ROLE_MAPPING);
storage.set(OFFICE_MAPPING_PATH, OFFICE_MAPPING);
Expand Down Expand Up @@ -55,10 +61,33 @@ function getRoleMapping(): Map<string, CamsRole> {
return roleMapping;
}

function addUstpDivisionMetaToMap(
map: Map<string, UstpDivisionMeta>,
meta: UstpDivisionMeta,
divisionCodes: string[],
) {
divisionCodes.forEach((divisionCode) => {
if (map.has(divisionCode)) {
map.set(divisionCode, { ...map.get(divisionCode), ...meta });
} else {
map.set(divisionCode, meta);
}
});
}

function getUstpDivisionMeta(): Map<string, UstpDivisionMeta> {
if (!metaMapping) {
metaMapping = new Map<string, UstpDivisionMeta>();
addUstpDivisionMetaToMap(metaMapping, { isLegacy: true }, LEGACY_DIVISION_CODES);
}
return metaMapping;
}

export const LocalStorageGateway: StorageGateway = {
get,
getUstpOffices,
getRoleMapping,
getUstpDivisionMeta,
};

export default LocalStorageGateway;
3 changes: 2 additions & 1 deletion backend/functions/lib/adapters/types/storage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { CamsRole } from '../../../../../common/src/cams/roles';
import { UstpOfficeDetails } from '../../../../../common/src/cams/offices';
import { UstpDivisionMeta, UstpOfficeDetails } from '../../../../../common/src/cams/offices';

export type StorageGateway = {
get(key: string): string | null;
getUstpOffices(): UstpOfficeDetails[];
getRoleMapping(): Map<string, CamsRole>;
getUstpDivisionMeta(): Map<string, UstpDivisionMeta>;
};
22 changes: 3 additions & 19 deletions backend/functions/lib/controllers/orders/orders.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { mockCamsHttpRequest } from '../../testing/mock-data/cams-http-request-h
import { ResponseBody } from '../../../../../common/src/api/response';
import { NotFoundError } from '../../common-errors/not-found-error';
import { BadRequestError } from '../../common-errors/bad-request';
import * as crypto from 'crypto';

const syncResponse: SyncOrdersStatus = {
options: {
Expand Down Expand Up @@ -120,7 +121,7 @@ describe('orders controller tests', () => {
const failTransfer = { ...orderTransfer };
failTransfer.id = crypto.randomUUID().toString();
const controller = new OrdersController(applicationContext);
expect(
await expect(
async () => await controller.updateOrder(applicationContext, id, failTransfer),
).rejects.toThrow(expectedError);
});
Expand Down Expand Up @@ -160,23 +161,6 @@ describe('orders controller tests', () => {
await expect(controller.getSuggestedCases(applicationContext)).rejects.toThrow(camsError);
});

test('should throw UnknownError if any other error is encountered', async () => {
const originalError = new Error('Test');
const unknownError = new UnknownError('TEST', { originalError });
jest.spyOn(OrdersUseCase.prototype, 'getOrders').mockRejectedValue(originalError);
jest.spyOn(OrdersUseCase.prototype, 'updateTransferOrder').mockRejectedValue(originalError);
jest.spyOn(OrdersUseCase.prototype, 'syncOrders').mockRejectedValue(originalError);
jest.spyOn(OrdersUseCase.prototype, 'getSuggestedCases').mockRejectedValue(originalError);

const controller = new OrdersController(applicationContext);
await expect(controller.getOrders(applicationContext)).rejects.toThrow(unknownError);
await expect(controller.updateOrder(applicationContext, id, orderTransfer)).rejects.toThrow(
unknownError,
);
await expect(controller.syncOrders(applicationContext)).rejects.toThrow(unknownError);
applicationContext.request = mockCamsHttpRequest({ params: { caseId: 'mockId' } });
await expect(controller.getSuggestedCases(applicationContext)).rejects.toThrow(unknownError);
});
test('should throw UnknownError if any other error is encountered', async () => {
const originalError = new Error('Test');
const unknownError = new UnknownError('TEST', { originalError });
Expand Down Expand Up @@ -289,7 +273,7 @@ describe('orders controller exception tests', () => {
applicationContext.request = request;
const controller = new OrdersController(applicationContext);

expect(async () => await controller.handleRequest(applicationContext)).rejects.toThrow(
await expect(async () => await controller.handleRequest(applicationContext)).rejects.toThrow(
expectedError,
);
});
Expand Down
2 changes: 2 additions & 0 deletions backend/functions/lib/controllers/orders/orders.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
getOrdersGateway,
getOrdersRepository,
getRuntimeStateRepository,
getStorageGateway,
} from '../../factory';
import { OrdersUseCase, SyncOrdersOptions, SyncOrdersStatus } from '../../use-cases/orders/orders';
import {
Expand Down Expand Up @@ -46,6 +47,7 @@ export class OrdersController implements CamsController, CamsTimerController {
getOrdersGateway(context),
getRuntimeStateRepository<OrderSyncState>(context),
getConsolidationOrdersRepository(context),
getStorageGateway(context),
);
}

Expand Down
40 changes: 39 additions & 1 deletion backend/functions/lib/use-cases/offices/offices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import OktaUserGroupGateway from '../../adapters/gateways/okta/okta-user-group-g
import { UserGroupGatewayConfig } from '../../adapters/types/authorization';
import { CamsUserGroup, Staff } from '../../../../../common/src/cams/users';
import MockData from '../../../../../common/src/cams/test-utilities/mock-data';
import { USTP_OFFICES_ARRAY } from '../../../../../common/src/cams/offices';
import { USTP_OFFICES_ARRAY, UstpDivisionMeta } from '../../../../../common/src/cams/offices';
import { TRIAL_ATTORNEYS } from '../../../../../common/src/cams/test-utilities/attorneys.mock';
import AttorneysList from '../attorneys';
import { MockMongoRepository } from '../../testing/mock-gateways/mock-mongo.repository';
Expand Down Expand Up @@ -39,6 +39,44 @@ describe('offices use case tests', () => {
expect(offices).toEqual(USTP_OFFICES_ARRAY);
});

test('should flag legacy offices', async () => {
const useCase = new OfficesUseCase();
const manhattanOffice = USTP_OFFICES_ARRAY.find(
(office) => office.officeCode === 'USTP_CAMS_Region_2_Office_Manhattan',
);

const legacyDivisionCode = '087';
const officeWithLegacyFlag = { ...manhattanOffice };
officeWithLegacyFlag.groups[0].divisions.find(
(d) => d.divisionCode === legacyDivisionCode,
).isLegacy = true;
const expectedOffices = [officeWithLegacyFlag];

jest.spyOn(factory, 'getOfficesGateway').mockImplementation(() => {
return {
getOfficeName: jest.fn(),
getOffices: jest.fn().mockResolvedValue([manhattanOffice]),
};
});

jest.spyOn(factory, 'getStorageGateway').mockImplementation(() => {
return {
get: jest.fn(),
getRoleMapping: jest.fn(),
getUstpOffices: jest.fn(),
getUstpDivisionMeta: jest
.fn()
.mockReturnValue(
new Map<string, UstpDivisionMeta>([[legacyDivisionCode, { isLegacy: true }]]),
),
};
});

const offices = await useCase.getOffices(applicationContext);

expect(offices).toEqual(expectedOffices);
});

test('should return default attorneys with feature flag off', async () => {
const localContext = {
...applicationContext,
Expand Down
19 changes: 17 additions & 2 deletions backend/functions/lib/use-cases/offices/offices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,23 @@ const MODULE_NAME = 'OFFICES_USE_CASE';

export class OfficesUseCase {
public async getOffices(context: ApplicationContext): Promise<UstpOfficeDetails[]> {
const gateway = getOfficesGateway(context);
return gateway.getOffices(context);
const officesGateway = getOfficesGateway(context);
const offices = await officesGateway.getOffices(context);

const storageGateway = getStorageGateway(context);
const metas = storageGateway.getUstpDivisionMeta();

offices.forEach((ustpOffice) => {
ustpOffice.groups.forEach((group) => {
group.divisions.forEach((division) => {
if (metas.has(division.divisionCode)) {
division.isLegacy = metas.get(division.divisionCode).isLegacy;
}
});
});
});

return offices;
}

public async getOfficeAttorneys(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getCasesRepository,
getCasesGateway,
getConsolidationOrdersRepository,
getStorageGateway,
} from '../../factory';
import {
ConsolidationOrderActionApproval,
Expand Down Expand Up @@ -56,6 +57,7 @@ describe('Orders use case', () => {
ordersGateway,
runtimeStateRepo,
consolidationRepo,
getStorageGateway(mockContext),
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getOrdersGateway,
getOrdersRepository,
getRuntimeStateRepository,
getStorageGateway,
} from '../../factory';
import { CasesRepository, ConsolidationOrdersRepository } from '../gateways.types';
import { ApplicationContext } from '../../adapters/types/basic';
Expand Down Expand Up @@ -125,6 +126,8 @@ describe('orders use case tests', () => {
let ordersRepo;
let runtimeStateRepo;
let casesGateway;
let storageGateway;

const authorizedUser = MockData.getCamsUser({
roles: [CamsRole.DataVerifier],
offices: [REGION_02_GROUP_NY],
Expand All @@ -137,6 +140,7 @@ describe('orders use case tests', () => {
runtimeStateRepo = getRuntimeStateRepository(mockContext);
ordersRepo = getOrdersRepository(mockContext);
casesGateway = getCasesGateway(mockContext);
storageGateway = getStorageGateway(mockContext);
});

test('should not create a second lead case for an existing consolidation', async () => {
Expand All @@ -159,6 +163,7 @@ describe('orders use case tests', () => {
ordersGateway,
runtimeStateRepo,
localConsolidationsRepo,
storageGateway,
);

// attempt to set up a consolidation with a different lead case
Expand Down
58 changes: 58 additions & 0 deletions backend/functions/lib/use-cases/orders/orders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
getCasesRepository,
getCasesGateway,
getConsolidationOrdersRepository,
getStorageGateway,
} from '../../factory';
import * as factory from '../../factory';
import { OrderSyncState } from '../gateways.types';
import {
ConsolidationOrder,
Expand Down Expand Up @@ -41,6 +43,7 @@ import { CaseAssignmentUseCase } from '../case-assignment';
import { REGION_02_GROUP_NY } from '../../../../../common/src/cams/test-utilities/mock-user';
import { getCourtDivisionCodes } from '../../../../../common/src/cams/users';
import { MockMongoRepository } from '../../testing/mock-gateways/mock-mongo.repository';
import { UstpDivisionMeta } from '../../../../../common/src/cams/offices';

describe('Orders use case', () => {
const CASE_ID = '000-11-22222';
Expand All @@ -51,6 +54,8 @@ describe('Orders use case', () => {
let runtimeStateRepo;
let casesGateway;
let consolidationRepo;
let storageGateway;

let useCase: OrdersUseCase;
const authorizedUser = MockData.getCamsUser({
roles: [CamsRole.DataVerifier],
Expand All @@ -67,13 +72,15 @@ describe('Orders use case', () => {
casesRepo = getCasesRepository(mockContext);
casesGateway = getCasesGateway(mockContext);
consolidationRepo = getConsolidationOrdersRepository(mockContext);
storageGateway = getStorageGateway(mockContext);
useCase = new OrdersUseCase(
casesRepo,
casesGateway,
ordersRepo,
ordersGateway,
runtimeStateRepo,
consolidationRepo,
storageGateway,
);
});

Expand Down Expand Up @@ -639,4 +646,55 @@ describe('Orders use case', () => {
expect.objectContaining({ updatedBy: getCamsUserReference(authorizedUser) }),
);
});

test('should fail to update to a legacy office', async () => {
const courtDivisionCode = '000';
jest.spyOn(factory, 'getStorageGateway').mockImplementation(() => {
return {
get: jest.fn(),
getRoleMapping: jest.fn(),
getUstpOffices: jest.fn(),
getUstpDivisionMeta: jest.fn().mockImplementation(() => {
return new Map<string, UstpDivisionMeta>([[courtDivisionCode, { isLegacy: true }]]);
}),
};
});

const localUseCase = new OrdersUseCase(
casesRepo,
casesGateway,
ordersRepo,
ordersGateway,
runtimeStateRepo,
consolidationRepo,
factory.getStorageGateway(mockContext),
);

const newCase = MockData.getCaseSummary({ override: { courtDivisionCode } });
const order: TransferOrder = MockData.getTransferOrder({
override: { status: 'approved', newCase },
});

const action: TransferOrderAction = {
id: order.id,
orderType: 'transfer',
caseId: order.caseId,
newCase: order.newCase,
status: 'approved',
};

const updateOrderFn = jest.spyOn(ordersRepo, 'update').mockResolvedValue({ id: 'mock-guid' });
const getOrderFn = jest.spyOn(ordersRepo, 'read').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: authorizedUser });

await expect(localUseCase.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();
});
});
Loading

0 comments on commit 5cb3d35

Please sign in to comment.