Skip to content

Commit

Permalink
Merge branch 'main' into PIMS-1951-Remove-pimshelp
Browse files Browse the repository at this point in the history
  • Loading branch information
dbarkowsky authored Aug 2, 2024
2 parents e258d7b + 8fcbce4 commit 288a703
Show file tree
Hide file tree
Showing 14 changed files with 319 additions and 19 deletions.
14 changes: 12 additions & 2 deletions express-api/src/controllers/projects/projectsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import userServices from '@/services/users/usersServices';
import { isAdmin, isAuditor, checkUserAgencyPermission } from '@/utilities/authorizationChecks';
import { DeepPartial } from 'typeorm';
import { Project } from '@/typeorm/Entities/Project';
import notificationServices from '@/services/notifications/notificationServices';

/**
* @description Get disposal project by either the numeric id or projectNumber.
Expand Down Expand Up @@ -101,9 +102,18 @@ export const deleteDisposalProject = async (req: Request, res: Response) => {
if (isNaN(projectId)) {
return res.status(400).send('Invalid Project ID');
}
// Only admins can delete projects
if (!isAdmin(req.user)) {
return res.status(403).send('Projects can only be deleted by Administrator role.');
}

const delProject = await projectServices.deleteProjectById(
projectId,
req.user.preferred_username,
);
const notifications = await notificationServices.cancelAllProjectNotifications(projectId);

const delProject = projectServices.deleteProjectById(projectId, req.user.preferred_username);
return res.status(200).send(delProject);
return res.status(200).send({ project: delProject, notifications });
};

/**
Expand Down
1 change: 1 addition & 0 deletions express-api/src/services/ches/chesServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export interface IChesStatusResponse {
txId: string;
updatedTS: number;
createdTS: number;
msgId: string;
}

/**
Expand Down
32 changes: 31 additions & 1 deletion express-api/src/services/notifications/notificationServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { ProjectStatusNotification } from '@/typeorm/Entities/ProjectStatusNotif
import { User } from '@/typeorm/Entities/User';
import { UUID, randomUUID } from 'crypto';
import nunjucks from 'nunjucks';
import { IsNull, QueryRunner } from 'typeorm';
import { In, IsNull, QueryRunner } from 'typeorm';
import chesServices, {
EmailBody,
EmailEncoding,
EmailPriority,
IChesStatusResponse,
IEmail,
} from '../ches/chesServices';
import { SSOUser } from '@bcgov/citz-imb-sso-express';
Expand Down Expand Up @@ -521,13 +522,42 @@ const getProjectNotificationsInQueue = async (
return pageModel;
};

const cancelAllProjectNotifications = async (projectId: number) => {
const notifications = await AppDataSource.getRepository(NotificationQueue).find({
where: [
{ ProjectId: projectId, Status: NotificationStatus.Accepted },
{ ProjectId: projectId, Status: NotificationStatus.Pending },
],
});
const chesCancelPromises = notifications.map((notification) => {
return chesServices.cancelEmailByIdAsync(notification.ChesMessageId);
});
const chesCancelResolved = await Promise.allSettled(chesCancelPromises);
const cancelledMessageIds = chesCancelResolved
.filter((a) => a.status === 'fulfilled' && a.value.status === 'cancelled')
.map((c) => (c as PromiseFulfilledResult<IChesStatusResponse>).value.msgId);
await AppDataSource.getRepository(NotificationQueue).update(
{ ChesMessageId: In(cancelledMessageIds) },
{ Status: convertChesStatusToNotificationStatus('cancelled') },
);
return {
succeeded: chesCancelResolved.filter(
(c) => c.status === 'fulfilled' && c.value.status === 'cancelled',
).length,
failed: chesCancelResolved.filter(
(c) => c.status === 'rejected' || c.value?.status !== 'cancelled',
).length,
};
};

const notificationServices = {
generateProjectNotifications,
generateAccessRequestNotification,
sendNotification,
updateNotificationStatus,
getProjectNotificationsInQueue,
convertChesStatusToNotificationStatus,
cancelAllProjectNotifications,
};

export default notificationServices;
6 changes: 0 additions & 6 deletions express-api/src/services/projects/projectsServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -794,12 +794,6 @@ const deleteProjectById = async (id: number, username: string) => {
{ ProjectId: id },
{ DeletedById: user.Id, DeletedOn: new Date() },
);
// Remove Notifications from Project
/* FIXME: This should eventually be done with the notifications service.
* Otherwise, any notifications sent to CHES won't be cancelled. -Dylan
* This is true ^ I think it's best to comment out this delete call for now. -Graham
*/
// await queryRunner.manager.delete(NotificationQueue, { ProjectId: id });
// Delete the project
const deleteResult = await queryRunner.manager.update(
Project,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { ViewColumn, ViewEntity } from 'typeorm';
SELECT b.id AS "building_id", b.pid, b.pin, p.id as "parcel_id"
FROM building b
LEFT OUTER JOIN parcel p
ON b.pid = p.pid OR b.pin = p.pin;
ON (b.pid = p.pid OR b.pin = p.pin) AND p.deleted_on IS NULL
WHERE b.deleted_on IS NULL;
`,
})
export class BuildingRelations {
Expand Down
4 changes: 2 additions & 2 deletions express-api/src/typeorm/Entities/views/MapPropertiesView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { ViewColumn, ViewEntity } from 'typeorm';
SELECT c.id, c.pid, c.pin, c.location, c.property_type_id, c.address1, c.classification_id, c.agency_id, c.is_visible_to_other_agencies, c.administrative_area_id, c.name, aa.regional_district_id as regional_district_id
FROM (
SELECT id, pid, pin, location, property_type_id, address1, classification_id, agency_id, is_visible_to_other_agencies, administrative_area_id, name
FROM parcel
FROM parcel WHERE deleted_on IS NULL
UNION ALL
SELECT id, pid, pin, location, property_type_id, address1, classification_id, agency_id, is_visible_to_other_agencies, administrative_area_id, name
FROM building
FROM building WHERE deleted_on IS NULL
) c
LEFT JOIN administrative_area aa ON c.administrative_area_id = aa.id;
`,
Expand Down
4 changes: 3 additions & 1 deletion express-api/src/typeorm/Entities/views/PropertyUnionView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { ViewColumn, ViewEntity } from 'typeorm';
updated_on,
land_area
FROM parcel p
WHERE deleted_on IS NULL
UNION ALL
SELECT
'Building' AS property_type,
Expand All @@ -30,7 +31,8 @@ SELECT
is_sensitive,
updated_on,
NULL AS land_area
FROM building b)
FROM building b
WHERE deleted_on IS NULL)
SELECT
property.*,
agc."name" AS agency_name,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class PropertyViewsMissingDeletedOnClause1722362022034 implements MigrationInterface {
name = 'PropertyViewsMissingDeletedOnClause1722362022034';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`,
['VIEW', 'property_union', 'public'],
);
await queryRunner.query(`DROP VIEW "property_union"`);
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`,
['VIEW', 'map_properties', 'public'],
);
await queryRunner.query(`DROP VIEW "map_properties"`);
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`,
['MATERIALIZED_VIEW', 'building_relations', 'public'],
);
await queryRunner.query(`DROP MATERIALIZED VIEW "building_relations"`);
await queryRunner.query(`CREATE VIEW "building_relations" AS
SELECT b.id AS "building_id", b.pid, b.pin, p.id as "parcel_id"
FROM building b
LEFT OUTER JOIN parcel p
ON (b.pid = p.pid OR b.pin = p.pin) AND p.deleted_on IS NULL
WHERE b.deleted_on IS NULL;
`);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`,
[
'public',
'VIEW',
'building_relations',
'SELECT b.id AS "building_id", b.pid, b.pin, p.id as "parcel_id"\n FROM building b\n LEFT OUTER JOIN parcel p\n ON (b.pid = p.pid OR b.pin = p.pin) AND p.deleted_on IS NULL\n WHERE b.deleted_on IS NULL;',
],
);
await queryRunner.query(`CREATE VIEW "map_properties" AS
SELECT c.id, c.pid, c.pin, c.location, c.property_type_id, c.address1, c.classification_id, c.agency_id, c.is_visible_to_other_agencies, c.administrative_area_id, c.name, aa.regional_district_id as regional_district_id
FROM (
SELECT id, pid, pin, location, property_type_id, address1, classification_id, agency_id, is_visible_to_other_agencies, administrative_area_id, name
FROM parcel WHERE deleted_on IS NULL
UNION ALL
SELECT id, pid, pin, location, property_type_id, address1, classification_id, agency_id, is_visible_to_other_agencies, administrative_area_id, name
FROM building WHERE deleted_on IS NULL
) c
LEFT JOIN administrative_area aa ON c.administrative_area_id = aa.id;
`);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`,
[
'public',
'VIEW',
'map_properties',
'SELECT c.id, c.pid, c.pin, c.location, c.property_type_id, c.address1, c.classification_id, c.agency_id, c.is_visible_to_other_agencies, c.administrative_area_id, c.name, aa.regional_district_id as regional_district_id\n FROM (\n SELECT id, pid, pin, location, property_type_id, address1, classification_id, agency_id, is_visible_to_other_agencies, administrative_area_id, name \n FROM parcel WHERE deleted_on IS NULL\n UNION ALL\n SELECT id, pid, pin, location, property_type_id, address1, classification_id, agency_id, is_visible_to_other_agencies, administrative_area_id, name \n FROM building WHERE deleted_on IS NULL\n ) c\n LEFT JOIN administrative_area aa ON c.administrative_area_id = aa.id;',
],
);
await queryRunner.query(`CREATE VIEW "property_union" AS WITH property AS (SELECT
'Parcel' AS property_type,
property_type_id,
id,
classification_id,
pid,
pin,
agency_id,
address1,
administrative_area_id,
is_sensitive,
updated_on,
land_area
FROM parcel p
WHERE deleted_on IS NULL
UNION ALL
SELECT
'Building' AS property_type,
property_type_id,
id,
classification_id,
pid,
pin,
agency_id,
address1,
administrative_area_id,
is_sensitive,
updated_on,
NULL AS land_area
FROM building b
WHERE deleted_on IS NULL)
SELECT
property.*,
agc."name" AS agency_name,
aa."name" AS administrative_area_name,
pc."name" AS property_classification_name
FROM property
LEFT JOIN agency agc ON property.agency_id = agc.id
LEFT JOIN administrative_area aa ON property.administrative_area_id = aa.id
LEFT JOIN property_classification pc ON property.classification_id = pc.id;`);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`,
[
'public',
'VIEW',
'property_union',
'WITH property AS (SELECT \n\t\'Parcel\' AS property_type,\n property_type_id,\n\tid,\n\tclassification_id,\n\tpid,\n\tpin,\n\tagency_id,\n\taddress1,\n\tadministrative_area_id,\n\tis_sensitive,\n\tupdated_on,\n\tland_area\nFROM parcel p\nWHERE deleted_on IS NULL\nUNION ALL\nSELECT \n\t\'Building\' AS property_type,\n property_type_id,\n\tid,\n\tclassification_id,\n\tpid,\n\tpin,\n\tagency_id,\n\taddress1,\n\tadministrative_area_id,\n\tis_sensitive,\n\tupdated_on,\n\tNULL AS land_area\nFROM building b\nWHERE deleted_on IS NULL)\nSELECT \n\tproperty.*, \n\tagc."name" AS agency_name,\n\taa."name" AS administrative_area_name,\n\tpc."name" AS property_classification_name\nFROM property \n\tLEFT JOIN agency agc ON property.agency_id = agc.id\n\tLEFT JOIN administrative_area aa ON property.administrative_area_id = aa.id\n\tLEFT JOIN property_classification pc ON property.classification_id = pc.id;',
],
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`,
['VIEW', 'property_union', 'public'],
);
await queryRunner.query(`DROP VIEW "property_union"`);
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`,
['VIEW', 'map_properties', 'public'],
);
await queryRunner.query(`DROP VIEW "map_properties"`);
await queryRunner.query(
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`,
['VIEW', 'building_relations', 'public'],
);
await queryRunner.query(`DROP VIEW "building_relations"`);
await queryRunner.query(`CREATE MATERIALIZED VIEW "building_relations" AS SELECT b.id AS "building_id", b.pid, b.pin, p.id as "parcel_id"
FROM building b
LEFT OUTER JOIN parcel p
ON b.pid = p.pid OR b.pin = p.pin;`);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`,
[
'public',
'MATERIALIZED_VIEW',
'building_relations',
'SELECT b.id AS "building_id", b.pid, b.pin, p.id as "parcel_id"\n FROM building b\n LEFT OUTER JOIN parcel p\n ON b.pid = p.pid OR b.pin = p.pin;',
],
);
await queryRunner.query(`CREATE VIEW "map_properties" AS SELECT c.id, c.pid, c.pin, c.location, c.property_type_id, c.address1, c.classification_id, c.agency_id, c.is_visible_to_other_agencies, c.administrative_area_id, c.name, aa.regional_district_id as regional_district_id
FROM (
SELECT id, pid, pin, location, property_type_id, address1, classification_id, agency_id, is_visible_to_other_agencies, administrative_area_id, name
FROM parcel
UNION ALL
SELECT id, pid, pin, location, property_type_id, address1, classification_id, agency_id, is_visible_to_other_agencies, administrative_area_id, name
FROM building
) c
LEFT JOIN administrative_area aa ON c.administrative_area_id = aa.id;`);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`,
[
'public',
'VIEW',
'map_properties',
'SELECT c.id, c.pid, c.pin, c.location, c.property_type_id, c.address1, c.classification_id, c.agency_id, c.is_visible_to_other_agencies, c.administrative_area_id, c.name, aa.regional_district_id as regional_district_id\n FROM (\n SELECT id, pid, pin, location, property_type_id, address1, classification_id, agency_id, is_visible_to_other_agencies, administrative_area_id, name \n FROM parcel\n UNION ALL\n SELECT id, pid, pin, location, property_type_id, address1, classification_id, agency_id, is_visible_to_other_agencies, administrative_area_id, name \n FROM building\n ) c\n LEFT JOIN administrative_area aa ON c.administrative_area_id = aa.id;',
],
);
await queryRunner.query(`CREATE VIEW "property_union" AS WITH property AS (SELECT
'Parcel' AS property_type,
property_type_id,
id,
classification_id,
pid,
pin,
agency_id,
address1,
administrative_area_id,
is_sensitive,
updated_on,
land_area
FROM parcel p
UNION ALL
SELECT
'Building' AS property_type,
property_type_id,
id,
classification_id,
pid,
pin,
agency_id,
address1,
administrative_area_id,
is_sensitive,
updated_on,
NULL AS land_area
FROM building b)
SELECT
property.*,
agc."name" AS agency_name,
aa."name" AS administrative_area_name,
pc."name" AS property_classification_name
FROM property
LEFT JOIN agency agc ON property.agency_id = agc.id
LEFT JOIN administrative_area aa ON property.administrative_area_id = aa.id
LEFT JOIN property_classification pc ON property.classification_id = pc.id;`);
await queryRunner.query(
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`,
[
'public',
'VIEW',
'property_union',
'WITH property AS (SELECT \n\t\'Parcel\' AS property_type,\n property_type_id,\n\tid,\n\tclassification_id,\n\tpid,\n\tpin,\n\tagency_id,\n\taddress1,\n\tadministrative_area_id,\n\tis_sensitive,\n\tupdated_on,\n\tland_area\nFROM parcel p\nUNION ALL\nSELECT \n\t\'Building\' AS property_type,\n property_type_id,\n\tid,\n\tclassification_id,\n\tpid,\n\tpin,\n\tagency_id,\n\taddress1,\n\tadministrative_area_id,\n\tis_sensitive,\n\tupdated_on,\n\tNULL AS land_area\nFROM building b)\nSELECT \n\tproperty.*, \n\tagc."name" AS agency_name,\n\taa."name" AS administrative_area_name,\n\tpc."name" AS property_classification_name\nFROM property \n\tLEFT JOIN agency agc ON property.agency_id = agc.id\n\tLEFT JOIN administrative_area aa ON property.administrative_area_id = aa.id\n\tLEFT JOIN property_classification pc ON property.classification_id = pc.id;',
],
);
}
}
1 change: 1 addition & 0 deletions express-api/tests/testUtils/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ export const produceEmailStatus = (props: Partial<IChesStatusResponse>): IChesSt
txId: props.txId ?? faker.string.uuid(),
updatedTS: new Date().getTime(),
createdTS: new Date().getTime(),
msgId: props.msgId ?? faker.string.uuid(),
};
return email;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Project } from '@/typeorm/Entities/Project';
import { ProjectSchema } from '@/controllers/projects/projectsSchema';
import { ProjectProperty } from '@/typeorm/Entities/ProjectProperty';
import { DeleteResult } from 'typeorm';
import { faker } from '@faker-js/faker';

const agencyRepo = AppDataSource.getRepository(Agency);

Expand Down Expand Up @@ -60,6 +61,13 @@ jest.mock('@/services/users/usersServices', () => ({
hasAgencies: jest.fn(() => _hasAgencies()),
}));

jest.mock('@/services/notifications/notificationServices', () => ({
cancelAllProjectNotifications: () => ({
succeeded: faker.number.int(),
failed: faker.number.int(),
}),
}));

jest
.spyOn(AppDataSource.getRepository(ProjectProperty), 'find')
.mockImplementation(async () => [
Expand Down Expand Up @@ -304,6 +312,7 @@ describe('UNIT - Testing controllers for users routes.', () => {
describe('DELETE /projects/disposal/:projectId', () => {
it('should return status 200 on successful deletion', async () => {
mockRequest.params.projectId = '1';
mockRequest.setUser({ client_roles: [Roles.ADMIN] });
await controllers.deleteDisposalProject(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(200);
});
Expand Down
Loading

0 comments on commit 288a703

Please sign in to comment.