Skip to content

Commit

Permalink
PIMS-1946 Cancel/Stop Notifications (#2726)
Browse files Browse the repository at this point in the history
Co-authored-by: LawrenceLau2020 <[email protected]>
  • Loading branch information
dbarkowsky and LawrenceLau2020 authored Oct 21, 2024
1 parent b3f0c8a commit 9320dd4
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 77 deletions.
2 changes: 1 addition & 1 deletion express-api/src/constants/switches.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { NODE_ENV } = process.env;

export default {
TESTING: NODE_ENV === 'test', // Can be used to disable certain features when testing
TESTING: NODE_ENV === 'test' || process.env.TESTING === 'true', // Can be used to disable certain features when testing
};
81 changes: 65 additions & 16 deletions express-api/src/services/projects/projectsServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ import {
import { ProjectFilter } from '@/services/projects/projectSchema';
import { PropertyType } from '@/constants/propertyType';
import { ProjectRisk } from '@/constants/projectRisk';
import notificationServices, { AgencyResponseType } from '../notifications/notificationServices';
import notificationServices, {
AgencyResponseType,
NotificationStatus,
} from '../notifications/notificationServices';
import {
constructFindOptionFromQuery,
constructFindOptionFromQuerySingleSelect,
Expand Down Expand Up @@ -413,6 +416,7 @@ const handleProjectNotifications = async (
Id: projectId,
},
});

const projectAgency = await queryRunner.manager.findOne(Agency, {
where: { Id: projectWithRelations.AgencyId },
});
Expand All @@ -427,23 +431,65 @@ const handleProjectNotifications = async (
projectWithRelations.Notes = projectNotes;

const notifsToSend: Array<NotificationQueue> = [];
const queueNotifications = async () => {
// If the status has been changed
if (previousStatus !== projectWithRelations.StatusId) {
// Has the project previously been to this status? If so, don't re-queue notifications.
const previousStatuses = await queryRunner.manager.find(ProjectStatusHistory, {
where: { ProjectId: projectWithRelations.Id },
});
const statusPreviouslyVisited = previousStatuses.some(
(record: ProjectStatusHistory) => record.StatusId === projectWithRelations.StatusId,
);
if (!statusPreviouslyVisited) {
const statusChangeNotifs = await notificationServices.generateProjectNotifications(
projectWithRelations,
previousStatus,
queryRunner,
);
return statusChangeNotifs;
}
}
return [];
};

if (previousStatus !== projectWithRelations.StatusId) {
const statusChangeNotifs = await notificationServices.generateProjectNotifications(
projectWithRelations,
previousStatus,
queryRunner,
);
notifsToSend.push(...statusChangeNotifs);
}
const queueWatchNotifications = async () => {
if (projectAgencyResponses.length) {
const agencyResponseNotifs = await notificationServices.generateProjectWatchNotifications(
projectWithRelations,
responses,
queryRunner,
);
return agencyResponseNotifs;
}
return [];
};

if (projectAgencyResponses.length) {
const agencyResponseNotifs = await notificationServices.generateProjectWatchNotifications(
projectWithRelations,
responses,
queryRunner,
// If the project is cancelled, cancel pending notifications
if (projectWithRelations.StatusId === ProjectStatus.CANCELLED) {
const pendingNotifications = await queryRunner.manager.find(NotificationQueue, {
where: [
{
ProjectId: projectWithRelations.Id,
Status: NotificationStatus.Accepted,
},
{
ProjectId: projectWithRelations.Id,
Status: NotificationStatus.Pending,
},
],
});

await Promise.all(
pendingNotifications.map((notification) => {
notificationServices.cancelNotificationById(notification.Id, user);
}),
);
notifsToSend.push(...agencyResponseNotifs);
// Queue cancellation notifications
notifsToSend.push(...(await queueNotifications()));
} else {
notifsToSend.push(...(await queueNotifications()));
notifsToSend.push(...(await queueWatchNotifications()));
}

return Promise.all(
Expand Down Expand Up @@ -781,8 +827,10 @@ const updateProject = async (
const returnProject = await projectRepo.findOne({ where: { Id: originalProject.Id } });
return returnProject;
} catch (e) {
await queryRunner.rollbackTransaction();
logger.warn(e.message);
if (queryRunner.isTransactionActive) {
await queryRunner.rollbackTransaction();
}
if (e instanceof ErrorWithCode) throw e;
throw new ErrorWithCode(`Error updating project: ${e.message}`, 500);
} finally {
Expand Down Expand Up @@ -1050,6 +1098,7 @@ const projectServices = {
updateProject,
getProjects,
getProjectsForExport,
handleProjectNotifications,
};

export default projectServices;
151 changes: 91 additions & 60 deletions express-api/tests/unit/services/projects/projectsServices.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AppDataSource } from '@/appDataSource';
import { ProjectStatus } from '@/constants/projectStatus';
import { ProjectType } from '@/constants/projectType';
import { Roles } from '@/constants/roles';
import { NotificationStatus } from '@/services/notifications/notificationServices';
import projectServices from '@/services/projects/projectsServices';
import userServices from '@/services/users/usersServices';
import { Agency } from '@/typeorm/Entities/Agency';
Expand Down Expand Up @@ -244,11 +245,14 @@ jest
.mockImplementation(() => projectJoinQueryBuilder);

const _generateProjectWatchNotifications = jest.fn(async () => [produceNotificationQueue()]);
const _generateProjectNotifications = jest.fn(async () => [produceNotificationQueue()]);
const _cancelNotificationById = jest.fn(async () => produceNotificationQueue());
jest.mock('@/services/notifications/notificationServices', () => ({
generateProjectNotifications: jest.fn(async () => [produceNotificationQueue()]),
generateProjectNotifications: async () => _generateProjectNotifications(),
sendNotification: jest.fn(async () => produceNotificationQueue()),
generateProjectWatchNotifications: async () => _generateProjectWatchNotifications(),
NotificationStatus: { Accepted: 0, Pending: 1, Cancelled: 2, Failed: 3, Completed: 4 },
cancelNotificationById: async () => _cancelNotificationById,
}));

describe('UNIT - Project Services', () => {
Expand Down Expand Up @@ -616,70 +620,97 @@ describe('UNIT - Project Services', () => {
);
expect(_generateProjectWatchNotifications).toHaveBeenCalled();
});
});

describe('getProjects', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return projects based on filter conditions', async () => {
const filter = {
statusId: 1,
agencyId: [3],
quantity: 10,
page: 1,
market: '$12',
netBook: '$12',
agency: 'contains,aaa',
status: 'contains,aaa',
projectNumber: 'contains,aaa',
name: 'contains,Project',
updatedOn: 'before,' + new Date(),
updatedBy: 'Jane',
sortOrder: 'asc',
sortKey: 'Status',
quickFilter: 'hi',
};

// Call the service function
const projectsResponse = await projectServices.getProjects(filter); // Pass the mocked projectRepo
// Returned project should be the one based on the agency and status id in the filter
expect(projectsResponse.totalCount).toEqual(1);
expect(projectsResponse.data.length).toEqual(1);
describe('handleProjectNotifications', () => {
it('should not send notifications when status becomes Cancelled', async () => {
const project = produceProject({
AgencyResponses: [produceAgencyResponse()],
StatusId: ProjectStatus.CANCELLED,
CancelledOn: new Date(),
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const queryRunner: any = {
manager: {
findOne: async () => project,
find: () => [produceNotificationQueue({ Status: NotificationStatus.Pending })],
},
};
const result = await projectServices.handleProjectNotifications(
project.Id,
ProjectStatus.ON_HOLD,
[produceAgencyResponse()],
producePimsRequestUser(),
queryRunner,
);
expect(_generateProjectWatchNotifications).not.toHaveBeenCalled();
expect(_generateProjectNotifications).toHaveBeenCalled();
expect(result).toHaveLength(1);
});
});

describe('getProjectsForExport', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return projects based on filter conditions', async () => {
const filter = {
statusId: 1,
agencyId: [3],
quantity: 10,
page: 0,
};

_projectFind.mockImplementationOnce(async () => {
const mockProjects: Project[] = [
produceProject({ Id: 1, Name: 'Project 1', StatusId: 1, AgencyId: 3 }),
produceProject({ Id: 2, Name: 'Project 2', StatusId: 4, AgencyId: 14 }),
];
// Check if the project matches the filter conditions
return mockProjects.filter(
(project) =>
filter.statusId === project.StatusId && filter.agencyId.includes(project.AgencyId),
);
});

// Call the service function
const projects = await projectServices.getProjectsForExport(filter); // Pass the mocked projectRepo

// Assertions
expect(_projectFind).toHaveBeenCalled();
// Returned project should be the one based on the agency and status id in the filter
expect(projects.length).toEqual(1);
describe('getProjects', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return projects based on filter conditions', async () => {
const filter = {
statusId: 1,
agencyId: [3],
quantity: 10,
page: 1,
market: '$12',
netBook: '$12',
agency: 'contains,aaa',
status: 'contains,aaa',
projectNumber: 'contains,aaa',
name: 'contains,Project',
updatedOn: 'before,' + new Date(),
updatedBy: 'Jane',
sortOrder: 'asc',
sortKey: 'Status',
quickFilter: 'hi',
};

// Call the service function
const projectsResponse = await projectServices.getProjects(filter); // Pass the mocked projectRepo
// Returned project should be the one based on the agency and status id in the filter
expect(projectsResponse.totalCount).toEqual(1);
expect(projectsResponse.data.length).toEqual(1);
});
});

describe('getProjectsForExport', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return projects based on filter conditions', async () => {
const filter = {
statusId: 1,
agencyId: [3],
quantity: 10,
page: 0,
};

_projectFind.mockImplementationOnce(async () => {
const mockProjects: Project[] = [
produceProject({ Id: 1, Name: 'Project 1', StatusId: 1, AgencyId: 3 }),
produceProject({ Id: 2, Name: 'Project 2', StatusId: 4, AgencyId: 14 }),
];
// Check if the project matches the filter conditions
return mockProjects.filter(
(project) =>
filter.statusId === project.StatusId && filter.agencyId.includes(project.AgencyId),
);
});

// Call the service function
const projects = await projectServices.getProjectsForExport(filter); // Pass the mocked projectRepo

// Assertions
expect(_projectFind).toHaveBeenCalled();
// Returned project should be the one based on the agency and status id in the filter
expect(projects.length).toEqual(1);
});
});
});

0 comments on commit 9320dd4

Please sign in to comment.