Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CAMS-283 - Add syncOfficeStaff to Offices use case #918

Merged
merged 18 commits into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/functions/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ module.exports = {
'lib/cosmos-humble-objects/',
'lib/testing/mock-data/index.ts',
'lib/testing/local-data/',
'lib/testing/isolated-integration/',
'lib/testing/testing-utilities.ts',
'jest.*config.js',
'lib/adapters/gateways/okta/HumbleVerifier.ts',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ export default class ConsolidationOrdersCosmosDbRepository
throw new Error('Method not implemented.');
}

upsert(
_context: ApplicationContext,
_partitionKey: string,
_data: ConsolidationOrder,
): Promise<ConsolidationOrder> {
throw new Error('Method not implemented.');
}

put(context: ApplicationContext, data: ConsolidationOrder): Promise<ConsolidationOrder> {
return this.repo.put(context, data);
throw new Error('Method not implemented.');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ export class CosmosDbRepository<T> implements DocumentRepository<T> {
return this.execute<T>(context, lambdaToExecute);
}

public async upsert(context: ApplicationContext, partitionKey: string, data: T): Promise<T> {
const lambdaToExecute = async <T>(): Promise<T> => {
const { resource } = await this.cosmosDbClient
.database(this.cosmosConfig.databaseName)
.container(this.containerName)
.items.upsert(data, { partitionKey });

context.logger.debug(this.moduleName, `${typeof data} Inserted/Updated ${resource.id}`);
return resource;
};
return this.execute<T>(context, lambdaToExecute);
}

public async put(context: ApplicationContext, data: T): Promise<T> {
const lambdaToExecute = async <T>(): Promise<T> => {
const { resource } = await this.cosmosDbClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { createMockApplicationContext } from '../../testing/testing-utilities';
import MockData from '../../../../../common/src/cams/test-utilities/mock-data';
import { CosmosDbRepository } from './cosmos/cosmos.repository';
import { CamsRole } from '../../../../../common/src/cams/roles';
import { OfficeStaff } from '../../../../../common/src/cams/staff';
import { SYSTEM_USER_REFERENCE } from '../../../../../common/src/cams/auditable';
import { getCamsUserReference } from '../../../../../common/src/cams/session';

describe('offices cosmosDB repository tests', () => {
let context: ApplicationContext;
Expand All @@ -21,8 +24,20 @@ describe('offices cosmosDB repository tests', () => {

test('should query data with office code, staff doc type, and trial attorney role', async () => {
const officeCode = 'test-office';
const attorneys = MockData.buildArray(MockData.getAttorneyUser, 5);
jest.spyOn(MockHumbleQuery.prototype, 'fetchAll').mockResolvedValue({ resources: attorneys });
const attorneys = MockData.buildArray(MockData.getAttorneyUser, 5).map((user) =>
getCamsUserReference(user),
);
const staffDocs: OfficeStaff[] = attorneys.map((user) => {
return {
id: user.id,
documentType: 'OFFICE_STAFF',
officeCode,
...user,
updatedOn: '2024-10-01T00:00:00.000Z',
updatedBy: SYSTEM_USER_REFERENCE,
};
});
jest.spyOn(MockHumbleQuery.prototype, 'fetchAll').mockResolvedValue({ resources: staffDocs });
const querySpy = jest.spyOn(CosmosDbRepository.prototype, 'query');

const actual = await repo.getOfficeAttorneys(context, officeCode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ import { ApplicationContext } from '../types/basic';
import { AttorneyUser, CamsUserReference } from '../../../../../common/src/cams/users';
import { CosmosDbRepository } from './cosmos/cosmos.repository';
import { UstpOfficeDetails } from '../../../../../common/src/cams/courts';
import { OfficeStaff } from '../../../../../common/src/cams/staff';
import { CamsRole } from '../../../../../common/src/cams/roles';
import { createAuditRecord } from '../../../../../common/src/cams/auditable';
import { getCamsUserReference } from '../../../../../common/src/cams/session';

const MODULE_NAME: string = 'COSMOS_DB_REPOSITORY_OFFICES';
const CONTAINER_NAME: string = 'offices';

export class OfficesCosmosDbRepository implements OfficesRepository {
private officeStaffRepo: CosmosDbRepository<CamsUserReference>;
private officeStaffRepo: CosmosDbRepository<OfficeStaff>;
private officesRepo: CosmosDbRepository<UstpOfficeDetails>;

constructor(context: ApplicationContext) {
this.officeStaffRepo = new CosmosDbRepository<CamsUserReference>(
this.officeStaffRepo = new CosmosDbRepository<OfficeStaff>(
context,
CONTAINER_NAME,
MODULE_NAME,
Expand All @@ -24,6 +27,19 @@ export class OfficesCosmosDbRepository implements OfficesRepository {
MODULE_NAME,
);
}
async putOfficeStaff(
context: ApplicationContext,
officeCode: string,
user: CamsUserReference,
): Promise<void> {
const staff = createAuditRecord<OfficeStaff>({
id: user.id,
documentType: 'OFFICE_STAFF',
officeCode,
...user,
});
await this.officeStaffRepo.upsert(context, officeCode, staff);
}

async getOfficeAttorneys(
context: ApplicationContext,
Expand All @@ -43,6 +59,7 @@ export class OfficesCosmosDbRepository implements OfficesRepository {
},
],
};
return await this.officeStaffRepo.query(context, querySpec);
const docs = await this.officeStaffRepo.query(context, querySpec);
return docs.map((doc) => getCamsUserReference(doc));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { InvocationContext } from '@azure/functions';
import applicationContextCreator from '../../../azure/application-context-creator';
import OktaUserGroupGateway from '../../adapters/gateways/okta/okta-user-group-gateway';
import { UserGroupGatewayConfig } from '../../adapters/types/authorization';
import { getUserGroupGatewayConfig } from '../../configs/user-groups-gateway-configuration';
import { OfficesUseCase } from '../../use-cases/offices/offices';
import { LoggerImpl } from '../../adapters/services/logger.service';

async function testOktaGroupApi() {
console.log('Isolated Integration Test: Okta Group Api', '\n');
Expand All @@ -16,6 +20,21 @@ async function testOktaGroupApi() {
const users = await OktaUserGroupGateway.getUserGroupUsers(config, group);
console.log(`${group.name} users`, users, '\n');
}

const context = await applicationContextCreator.getApplicationContext({
invocationContext: new InvocationContext(),
logger: new LoggerImpl('test-invocation'),
});
const useCase = new OfficesUseCase();
const results = await useCase.syncOfficeStaff(context);

const attorneys = await useCase.getOfficeAttorneys(
context,
'USTP_CAMS_Region_2_Office_Manhattan',
);
console.log('attorneys', attorneys, '\n');

console.log('syncOfficeStaff', JSON.stringify(results, null, 2), '\n');
} catch (error) {
console.error(error, '\n');
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export class LocalCosmosDbRepository<T extends Item> implements DocumentReposito
throw new Error('Method not implemented.');
}

upsert(_context: ApplicationContext, _partitionKey: string, _data: T): Promise<T> {
throw new Error('Method not implemented.');
}

async put(_context: ApplicationContext, data: T): Promise<T> {
const doc: T = { ...data, id: crypto.randomUUID() };
this.container.push(doc);
Expand Down
8 changes: 7 additions & 1 deletion backend/functions/lib/use-cases/gateways.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { CaseAssignmentHistory, CaseHistory } from '../../../../common/src/cams/history';
import { CaseDocket } from '../../../../common/src/cams/cases';
import { OrdersSearchPredicate } from '../../../../common/src/api/search';
import { AttorneyUser } from '../../../../common/src/cams/users';
import { AttorneyUser, CamsUserReference } from '../../../../common/src/cams/users';

export interface RepositoryResource {
id?: string;
Expand All @@ -23,6 +23,7 @@ export interface RepositoryResource {
export interface DocumentRepository<T extends RepositoryResource> {
get(context: ApplicationContext, id: string, partitionKey: string): Promise<T>;
update(context: ApplicationContext, id: string, partitionKey: string, data: T);
upsert(context: ApplicationContext, partitionKey: string, data: T): Promise<T>;
put(context: ApplicationContext, data: T): Promise<T>;
putAll(context: ApplicationContext, list: T[]): Promise<T[]>;
delete(context: ApplicationContext, id: string, partitionKey: string);
Expand Down Expand Up @@ -82,6 +83,11 @@ export interface CasesRepository {

export interface OfficesRepository {
getOfficeAttorneys(context: ApplicationContext, officeCode: string): Promise<AttorneyUser[]>;
putOfficeStaff(
context: ApplicationContext,
officeCode: string,
user: CamsUserReference,
): Promise<void>;
}

// TODO: Move these models to a top level models file?
Expand Down
70 changes: 68 additions & 2 deletions backend/functions/lib/use-cases/offices/offices.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { OfficeDetails } from '../../../../../common/src/cams/courts';
import { OfficeDetails, UstpOfficeDetails } from '../../../../../common/src/cams/courts';
import { CamsUserReference } from '../../../../../common/src/cams/users';
import { ApplicationContext } from '../../adapters/types/basic';
import { getOfficesGateway, getOfficesRepository } from '../../factory';
import {
getOfficesGateway,
getUserGroupGateway,
getOfficesRepository,
getStorageGateway,
} from '../../factory';
import { AttorneyUser } from '../../../../../common/src/cams/users';

export class OfficesUseCase {
Expand All @@ -16,4 +22,64 @@ export class OfficesUseCase {
const repository = getOfficesRepository(context);
return repository.getOfficeAttorneys(context, officeCode);
}

public async syncOfficeStaff(context: ApplicationContext): Promise<object> {
const config = context.config.userGroupGatewayConfig;
const repository = getOfficesRepository(context);
const gateway = getUserGroupGateway(context);
const storage = getStorageGateway(context);

// Get IdP to CAMS mappings.
const offices = storage.getUstpOffices();
const groupToRoleMap = storage.getRoleMapping();
const groupToOfficeMap = [...offices.values()].reduce((acc, office) => {
acc.set(office.idpGroupId, office);
return acc;
}, new Map<string, UstpOfficeDetails>());

// Filter out any groups not relevant to CAMS.
const userGroups = await gateway.getUserGroups(config);
const officeGroups = userGroups.filter((group) => groupToOfficeMap.has(group.name));
const roleGroups = userGroups.filter((group) => groupToRoleMap.has(group.name));

// Map roles to users.
const userMap = new Map<string, CamsUserReference>();
for (const roleGroup of roleGroups) {
const users = await gateway.getUserGroupUsers(config, roleGroup);
const role = groupToRoleMap.get(roleGroup.name);
for (const user of users) {
if (userMap.has(user.id)) {
userMap.get(user.id).roles.push(role);
} else {
user.roles = [role];
userMap.set(user.id, user);
}
}
}

// Write users with roles to the repo for each office.
const officesWithUsers: UstpOfficeDetails[] = [];
for (const officeGroup of officeGroups) {
const office = { ...groupToOfficeMap.get(officeGroup.name), staff: [] };

const users = await gateway.getUserGroupUsers(config, officeGroup);
for (const user of users) {
if (!userMap.has(user.id)) {
userMap.set(user.id, user);
}
const userWithRoles = userMap.has(user.id) ? userMap.get(user.id) : user;
office.staff.push(userWithRoles);
await repository.putOfficeStaff(context, office.officeCode, userWithRoles);
}

officesWithUsers.push(office);
}

// TODO: What to do with users with roles WITHOUT offices?
return {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can chase this down in a follow on PR.

userGroups,
users: [...userMap.values()],
officesWithUsers,
};
}
}
8 changes: 8 additions & 0 deletions common/src/cams/staff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Auditable } from './auditable';
import { CamsUserReference } from './users';

export type OfficeStaff = CamsUserReference &
Auditable & {
documentType: 'OFFICE_STAFF';
officeCode: string;
};
Loading