Skip to content

Commit

Permalink
Merge branch 'main' into PIMS-1985-AgencySelectionOnAddProperty
Browse files Browse the repository at this point in the history
  • Loading branch information
GrahamS-Quartech authored Aug 15, 2024
2 parents 178f924 + 82cbf4b commit 4a6be49
Show file tree
Hide file tree
Showing 17 changed files with 361 additions and 41 deletions.
3 changes: 1 addition & 2 deletions express-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"dependencies": {
"@bcgov/citz-imb-kc-css-api": "https://github.com/bcgov/citz-imb-kc-css-api/releases/download/v1.4.0/bcgov-citz-imb-kc-css-api-1.4.0.tgz",
"@bcgov/citz-imb-sso-express": "1.0.1",
"axios": "1.7.1",
"body-parser": "1.20.2",
"compression": "1.7.4",
"cookie-parser": "1.4.6",
Expand All @@ -42,7 +41,7 @@
"tsconfig-paths": "4.2.0",
"typeorm": "0.3.20",
"typeorm-naming-strategies": "4.1.0",
"winston": "3.13.0",
"winston": "3.14.2",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
"zod": "3.23.3"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export const getNotificationsByProjectId = async (req: Request, res: Response) =

return res.status(200).send(notificationsResult);
} catch (error) {
// not sure if the error codes can be handled better here?
return res.status(500).send({ message: 'Error fetching notifications' });
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode';
import {
constructFindOptionFromQuery,
constructFindOptionFromQueryBoolean,
constructFindOptionFromQuerySingleSelect,
} from '@/utilities/helperFunctions';
import { AdministrativeAreaJoinView } from '@/typeorm/Entities/views/AdministrativeAreaJoinView';
import { SortOrders } from '@/constants/types';
Expand All @@ -32,7 +33,9 @@ const collectFindOptions = (filter: AdministrativeAreaFilter) => {
const options = [];
if (filter.name) options.push(constructFindOptionFromQuery('Name', filter.name));
if (filter.regionalDistrictName)
options.push(constructFindOptionFromQuery('RegionalDistrictName', filter.regionalDistrictName));
options.push(
constructFindOptionFromQuerySingleSelect('RegionalDistrictName', filter.regionalDistrictName),
);
if (filter.isDisabled)
options.push(constructFindOptionFromQueryBoolean('IsDisabled', filter.isDisabled));
if (filter.createdOn) options.push(constructFindOptionFromQuery('CreatedOn', filter.createdOn));
Expand Down
3 changes: 2 additions & 1 deletion express-api/src/services/agencies/agencyServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AgencyFilter } from './agencySchema';
import {
constructFindOptionFromQuery,
constructFindOptionFromQueryBoolean,
constructFindOptionFromQuerySingleSelect,
} from '@/utilities/helperFunctions';
import { Brackets, FindOptionsWhere } from 'typeorm';
import logger from '@/utilities/winstonLogger';
Expand All @@ -25,7 +26,7 @@ const collectFindOptions = (filter: AgencyFilter) => {
if (filter.isDisabled)
options.push(constructFindOptionFromQueryBoolean('IsDisabled', filter.isDisabled));
if (filter.parentName)
options.push(constructFindOptionFromQuery('ParentName', filter.parentName));
options.push(constructFindOptionFromQuerySingleSelect('ParentName', filter.parentName));
if (filter.sendEmail)
options.push(constructFindOptionFromQueryBoolean('SendEmail', filter.sendEmail));
if (filter.email) options.push(constructFindOptionFromQuery('Email', filter.email));
Expand Down
38 changes: 36 additions & 2 deletions express-api/src/services/notifications/notificationServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export enum NotificationStatus {
Cancelled = 2,
Failed = 3,
Completed = 4,
NotFound = 5,
}

export enum NotificationAudience {
Expand Down Expand Up @@ -442,6 +443,8 @@ const convertChesStatusToNotificationStatus = (chesStatus: string): Notification
return NotificationStatus.Failed;
case 'completed':
return NotificationStatus.Completed;
case '404':
return NotificationStatus.NotFound;
default:
return null;
}
Expand All @@ -465,7 +468,6 @@ const updateNotificationStatus = async (notificationId: number, user: User) => {
}

const statusResponse = await chesServices.getStatusByIdAsync(notification.ChesMessageId);

if (typeof statusResponse?.status === 'string') {
const notificationStatus = convertChesStatusToNotificationStatus(statusResponse.status);
// If the CHES status is non-standard, don't update the notification.
Expand All @@ -481,7 +483,39 @@ const updateNotificationStatus = async (notificationId: number, user: User) => {
query.release();
return updatedNotification;
} else if (typeof statusResponse?.status === 'number') {
//If we get number type then this wound up being some HTTP code.
// handle 404, 422 code here....
switch (statusResponse.status) {
case 404: {
notification.Status = NotificationStatus.NotFound;
notification.UpdatedOn = new Date();
notification.UpdatedById = user.Id;
const updatedNotification = await query.manager.save(NotificationQueue, notification);
logger.error(`Notification with id ${notificationId} not found on CHES.`);
query.release();
return updatedNotification;
}

case 422:
logger.error(
`Notification with id ${notificationId} could not be processed, some of the data could be formatted incorrectly.`,
);
break;

case 401:
logger.error(
`Cannot authorize the request to the CHES server, check your CHES credentials.`,
);
break;

case 500:
logger.error(
`Internal server error while retrieving status for notification with id ${notificationId}.`,
);
break;
default:
logger.error(`Received unexpected status code ${statusResponse.status}.`);
break;
}
query.release();
return notification;
} else {
Expand Down
11 changes: 8 additions & 3 deletions express-api/src/services/projects/projectsServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ import { ProjectRisk } from '@/constants/projectRisk';
import notificationServices, { AgencyResponseType } from '../notifications/notificationServices';
import { SSOUser } from '@bcgov/citz-imb-sso-express';
import userServices from '../users/usersServices';
import { constructFindOptionFromQuery } from '@/utilities/helperFunctions';
import {
constructFindOptionFromQuery,
constructFindOptionFromQuerySingleSelect,
} from '@/utilities/helperFunctions';
import { ProjectTimestamp } from '@/typeorm/Entities/ProjectTimestamp';
import { ProjectMonetary } from '@/typeorm/Entities/ProjectMonetary';
import { NotificationQueue } from '@/typeorm/Entities/NotificationQueue';
Expand Down Expand Up @@ -880,8 +883,10 @@ const deleteProjectById = async (id: number, username: string) => {
const collectFindOptions = (filter: ProjectFilter) => {
const options = [];
if (filter.name) options.push(constructFindOptionFromQuery('Name', filter.name));
if (filter.agency) options.push(constructFindOptionFromQuery('Agency', filter.agency));
if (filter.status) options.push(constructFindOptionFromQuery('Status', filter.status));
if (filter.agency)
options.push(constructFindOptionFromQuerySingleSelect('Agency', filter.agency));
if (filter.status)
options.push(constructFindOptionFromQuerySingleSelect('Status', filter.status));
if (filter.projectNumber) {
options.push(constructFindOptionFromQuery('ProjectNumber', filter.projectNumber));
}
Expand Down
12 changes: 8 additions & 4 deletions express-api/src/services/properties/propertiesServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { PropertyUnion } from '@/typeorm/Entities/views/PropertyUnionView';
import {
constructFindOptionFromQuery,
constructFindOptionFromQueryPid,
constructFindOptionFromQuerySingleSelect,
} from '@/utilities/helperFunctions';
import userServices from '../users/usersServices';
import { Brackets, FindManyOptions, FindOptionsWhere, ILike, In, QueryRunner } from 'typeorm';
Expand Down Expand Up @@ -660,18 +661,21 @@ const sortKeyTranslator: Record<string, string> = {
*/
const collectFindOptions = (filter: PropertyUnionFilter) => {
const options = [];
if (filter.agency) options.push(constructFindOptionFromQuery('Agency', filter.agency));
if (filter.agency)
options.push(constructFindOptionFromQuerySingleSelect('Agency', filter.agency));
if (filter.pid) options.push(constructFindOptionFromQueryPid('PID', filter.pid));
if (filter.pin) options.push(constructFindOptionFromQueryPid('PIN', filter.pin));
if (filter.address) options.push(constructFindOptionFromQuery('Address', filter.address));
if (filter.updatedOn) options.push(constructFindOptionFromQuery('UpdatedOn', filter.updatedOn));
if (filter.classification)
options.push(constructFindOptionFromQuery('Classification', filter.classification));
options.push(constructFindOptionFromQuerySingleSelect('Classification', filter.classification));
if (filter.landArea) options.push(constructFindOptionFromQuery('LandArea', filter.landArea));
if (filter.administrativeArea)
options.push(constructFindOptionFromQuery('AdministrativeArea', filter.administrativeArea));
options.push(
constructFindOptionFromQuerySingleSelect('AdministrativeArea', filter.administrativeArea),
);
if (filter.propertyType)
options.push(constructFindOptionFromQuery('PropertyType', filter.propertyType));
options.push(constructFindOptionFromQuerySingleSelect('PropertyType', filter.propertyType));
return options;
};

Expand Down
35 changes: 35 additions & 0 deletions express-api/src/utilities/helperFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,32 @@ export const constructFindOptionFromQueryBoolean = <T>(
return { [column]: internalMatcher(value) } as FindOptionsWhere<T>;
};

export const constructFindOptionFromQuerySingleSelect = <T>(
column: keyof T,
operatorValuePair: string,
): FindOptionsWhere<T> => {
if (operatorValuePair == null || operatorValuePair.match(/([^,]*),(.*)/) == null) {
return { [column]: undefined } as FindOptionsWhere<T>;
}
const [, operator, value] = operatorValuePair.match(/([^,]*),(.*)/).map((a) => a.trim());
const listOfValues = value.split(',');
let internalMatcher;
switch (operator) {
case 'is':
internalMatcher = Equal;
break;
case 'not':
internalMatcher = (str: string) => Not(Equal(str));
break;
case 'isAnyOf':
internalMatcher = () => IsAnyOfWrapper(listOfValues);
break;
default:
return { [column]: undefined } as FindOptionsWhere<T>;
}
return { [column]: internalMatcher(value) } as FindOptionsWhere<T>;
};

/**
* Accepts a column alias and produces a FindOptionsWhere style object.
* This lets you plug in the return value to typeorm functions such as .find, findOne, etc.
Expand Down Expand Up @@ -167,6 +193,15 @@ export const TimestampComparisonWrapper = (tsValue: string, operator: TimestampO
return Raw((alias) => `${alias} ${operator} '${toPostgresTimestamp(new Date(tsValue))}'`);
};

/**
* Simple wrapper to produce an IN ARRAY style query when you want to match against any of several different specific options.
* @param elements An array of elements to match against.
* @returns FindOptionsWhere<T>
*/
export const IsAnyOfWrapper = (elements: string[]) => {
return Raw((alias) => `${alias} IN (${elements.map((a) => `'${a}'`).join(',')})`);
};

//The behavior of the Raw function seems bugged under certain query formats.
//It will use the correct table alias name, but not the correct column.
//ie. It will pass Project.ProjectNumber instead of "Project_project_number" (correct column alias constructed by TypeORM)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { ProjectStatusNotification } from '@/typeorm/Entities/ProjectStatusNotif
import { User } from '@/typeorm/Entities/User';
import { Agency } from '@/typeorm/Entities/Agency';
import { ProjectStatusHistory } from '@/typeorm/Entities/ProjectStatusHistory';
import logger from '@/utilities/winstonLogger';

const _notifQueueSave = jest
.fn()
Expand Down Expand Up @@ -271,6 +272,121 @@ describe('updateNotificationStatus', () => {
// Expect that notification was not updated.
expect(response.Status).toBe(notification.Status);
});
it('should handle CHES status 404 and update notification status to NotFound', async () => {
const user = produceUser();
const notifQueue = produceNotificationQueue();
notifQueue.ChesMessageId = randomUUID();

_getStatusByIdAsync.mockResolvedValueOnce({
status: 404 as unknown as string,
tag: 'sampleTag',
txId: randomUUID(),
updatedTS: Date.now(),
createdTS: Date.now(),
msgId: randomUUID(),
});

const response = await notificationServices.updateNotificationStatus(notifQueue.Id, user);

expect(response.Status).toBe(NotificationStatus.NotFound);
expect(response.UpdatedById).toBe(user.Id);
});

it('should handle CHES status 422 and log an error', async () => {
const user = produceUser();
const notifQueue = produceNotificationQueue();
notifQueue.ChesMessageId = randomUUID();

_getStatusByIdAsync.mockResolvedValueOnce({
status: 422 as unknown as string,
tag: 'sampleTag',
txId: randomUUID(),
updatedTS: Date.now(),
createdTS: Date.now(),
msgId: randomUUID(),
});

const loggerErrorSpy = jest.spyOn(logger, 'error');

const response = await notificationServices.updateNotificationStatus(notifQueue.Id, user);

expect(response.Status).toBe(notifQueue.Status);
expect(loggerErrorSpy).toHaveBeenCalledWith(
`Notification with id ${notifQueue.Id} could not be processed, some of the data could be formatted incorrectly.`,
);
});

it('should handle CHES status 401 and log an error', async () => {
const user = produceUser();
const notifQueue = produceNotificationQueue();
notifQueue.ChesMessageId = randomUUID();

_getStatusByIdAsync.mockResolvedValueOnce({
status: 401 as unknown as string,
tag: 'sampleTag',
txId: randomUUID(),
updatedTS: Date.now(),
createdTS: Date.now(),
msgId: randomUUID(),
});

const loggerErrorSpy = jest.spyOn(logger, 'error');

const response = await notificationServices.updateNotificationStatus(notifQueue.Id, user);

expect(response.Status).toBe(notifQueue.Status);
expect(loggerErrorSpy).toHaveBeenCalledWith(
`Cannot authorize the request to the CHES server, check your CHES credentials.`,
);
});

it('should handle CHES status 500 and log an error', async () => {
const user = produceUser();
const notifQueue = produceNotificationQueue();
notifQueue.ChesMessageId = randomUUID();

_getStatusByIdAsync.mockResolvedValueOnce({
status: 500 as unknown as string,
tag: 'sampleTag',
txId: randomUUID(),
updatedTS: Date.now(),
createdTS: Date.now(),
msgId: randomUUID(),
});

const loggerErrorSpy = jest.spyOn(logger, 'error');

const response = await notificationServices.updateNotificationStatus(notifQueue.Id, user);

expect(response.Status).toBe(notifQueue.Status);
expect(loggerErrorSpy).toHaveBeenCalledWith(
`Internal server error while retrieving status for notification with id ${notifQueue.Id}.`,
);
});
it('should throw an error when getNotificationStatusById fails to retrieve the status', async () => {
const mockInvalidStatusResponse: IChesStatusResponse = {
status: null, // Simulating an invalid status
tag: '',
txId: '',
updatedTS: 1234,
createdTS: 1234,
msgId: '',
};
const user = produceUser();

// Mock the response of `getStatusByIdAsync` to return the invalid status response
jest.spyOn(chesServices, 'getStatusByIdAsync').mockResolvedValueOnce(mockInvalidStatusResponse);

// Mock the query manager findOne method to return a valid notification
const mockNotification = { Id: 1, ChesMessageId: 'some-message-id' };
const mockFindOne = jest.spyOn(AppDataSource.createQueryRunner().manager, 'findOne');
mockFindOne.mockResolvedValueOnce(mockNotification);

// Expect the updateNotificationStatus to throw an error
await expect(notificationServices.updateNotificationStatus(1, user)).rejects.toThrow(
'Failed to retrieve status for notification with id 1.',
);
});
});
describe('convertChesStatusToNotificationStatus', () => {
it('should return NotificationStatus.Accepted for "accepted"', () => {
Expand All @@ -297,6 +413,12 @@ describe('convertChesStatusToNotificationStatus', () => {
);
});

it('should return NotificationStatus.NotFound when chesStatus is "404"', () => {
expect(notificationServices.convertChesStatusToNotificationStatus('404')).toBe(
NotificationStatus.NotFound,
);
});

it('should return NotificationStatus.Completed for "completed"', () => {
expect(notificationServices.convertChesStatusToNotificationStatus('completed')).toBe(
NotificationStatus.Completed,
Expand Down
6 changes: 3 additions & 3 deletions react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"@mui/material": "5.16.0",
"@mui/x-data-grid": "7.12.0",
"@mui/x-date-pickers": "7.12.0",
"@turf/turf": "7.0.0",
"@turf/turf": "7.1.0",
"dayjs": "1.11.10",
"node-xlsx": "0.24.0",
"react": "18.3.1",
Expand Down Expand Up @@ -61,7 +61,7 @@
"ts-jest": "29.2.0",
"ts-node": "10.9.2",
"typescript": "5.5.3",
"vite": "5.3.1",
"vite-tsconfig-paths": "4.3.1"
"vite": "5.4.0",
"vite-tsconfig-paths": "5.0.1"
}
}
Loading

0 comments on commit 4a6be49

Please sign in to comment.