Skip to content

Commit

Permalink
Merge pull request #41 from PermanentOrg/send_archive_steward_notific…
Browse files Browse the repository at this point in the history
…ation

Send notification emails to archive stewards
  • Loading branch information
cecilia-donnelly authored Jun 14, 2023
2 parents 01de742 + eab75ad commit 6000d66
Show file tree
Hide file tree
Showing 11 changed files with 421 additions and 64 deletions.
12 changes: 11 additions & 1 deletion src/directive/queries/update_directive.sql
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
WITH original_directive AS (
SELECT
steward_account_id
FROM
directive
WHERE
directive_id = :directiveId
)
UPDATE
directive
SET
Expand Down Expand Up @@ -37,4 +45,6 @@ RETURNING
accountId = steward_account_id
) "steward",
note,
execution_dt "executionDt";
execution_dt "executionDt",
steward_account_id != (SELECT steward_account_id FROM original_directive) "stewardChanged";

39 changes: 39 additions & 0 deletions src/directive/service/create.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { NotFound, BadRequest, InternalServerError } from "http-errors";
import { db } from "../../database";
import { sendArchiveStewardNotification } from "../../email";
import { logger } from "../../log";
import { directiveService } from "./index";
import type { Directive, DirectiveTrigger } from "../model";

jest.mock("../../database");
jest.mock("../../email", () => ({
sendArchiveStewardNotification: jest.fn(),
}));
jest.mock("../../log", () => ({
logger: {
error: jest.fn(),
},
}));

const loadFixtures = async (): Promise<void> => {
await db.sql("fixtures.create_test_accounts");
Expand All @@ -24,9 +34,15 @@ describe("createDirective", () => {
});
afterEach(async () => {
await clearDatabase();
jest.clearAllMocks();
});

test("should successfully create a directive and trigger", async () => {
(
sendArchiveStewardNotification as jest.MockedFunction<
typeof sendArchiveStewardNotification
>
).mockResolvedValueOnce(undefined);
await directiveService.createDirective({
emailFromAuthToken: "test@permanent.org",
archiveId: "1",
Expand Down Expand Up @@ -69,6 +85,29 @@ describe("createDirective", () => {
{ directiveId: directiveResult.rows[0]?.directiveId }
);
expect(triggerResult.rows.length).toBe(1);
expect(sendArchiveStewardNotification).toHaveBeenCalledWith(
directiveResult.rows[0]?.directiveId
);
});

test("should log error if notification email fails", async () => {
const testError = new Error("out of cheese error - redo from start");
(
sendArchiveStewardNotification as jest.MockedFunction<
typeof sendArchiveStewardNotification
>
).mockRejectedValueOnce(testError);
await directiveService.createDirective({
emailFromAuthToken: "test@permanent.org",
archiveId: "1",
stewardEmail: "test@permanent.org",
type: "transfer",
trigger: {
type: "admin",
},
});

expect(logger.error).toHaveBeenCalledWith(testError);
});

test("should error if authenticated account doesn't own the archive", async () => {
Expand Down
7 changes: 7 additions & 0 deletions src/directive/service/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isMissingStewardAccountError,
getInvalidValueFromInvalidEnumMessage,
} from "../../database_util";
import { sendArchiveStewardNotification } from "../../email";
import { logger } from "../../log";
import { confirmArchiveOwnership } from "./utils";

Expand Down Expand Up @@ -80,5 +81,11 @@ export const createDirective = async (
return directive;
});

await sendArchiveStewardNotification(directiveToReturn.directiveId).catch(
(err) => {
logger.error(err);
}
);

return directiveToReturn;
};
80 changes: 80 additions & 0 deletions src/directive/service/update.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { NotFound, BadRequest, InternalServerError } from "http-errors";
import { db } from "../../database";
import { sendArchiveStewardNotification } from "../../email";
import { logger } from "../../log";
import { directiveService } from "./index";
import type { Directive, DirectiveTrigger } from "../model";

jest.mock("../../database");
jest.mock("../../email", () => ({
sendArchiveStewardNotification: jest.fn(),
}));
jest.mock("../../log", () => ({
logger: {
error: jest.fn(),
},
}));

const testDirectiveId = "39b2a5fa-3508-4030-91b6-21dc6ec7a1ab";
const testNote = "test note";
Expand All @@ -29,9 +39,15 @@ describe("updateDirective", () => {
});
afterEach(async () => {
await clearDatabase();
jest.clearAllMocks();
});

test("should successfully update steward account and note", async () => {
(
sendArchiveStewardNotification as jest.MockedFunction<
typeof sendArchiveStewardNotification
>
).mockResolvedValueOnce(undefined);
await directiveService.updateDirective(testDirectiveId, {
emailFromAuthToken: "test@permanent.org",
stewardEmail: "test+2@permanent.org",
Expand Down Expand Up @@ -88,6 +104,70 @@ describe("updateDirective", () => {
);
expect(triggerResult.rows.length).toBe(1);
expect(triggerResult.rows[0]?.type).toBe("admin");
expect(sendArchiveStewardNotification).toHaveBeenCalledWith(
testDirectiveId
);
});

test("should successfully update and note only", async () => {
await directiveService.updateDirective(testDirectiveId, {
emailFromAuthToken: "test@permanent.org",
note: testNote,
});

const directiveResult = await db.query<Directive>(
`SELECT
directive_id "directiveId",
archive_id "archiveId",
type,
created_dt "createdDt",
updated_dt "updatedDt",
(
SELECT
jsonb_build_object(
'email',
primaryEmail,
'name',
fullName
)
FROM
account
WHERE
accountId = steward_account_id
) "steward",
note,
execution_dt "executionDt"
FROM
directive
WHERE
archive_id = :archiveId`,
{ archiveId: 1 }
);
expect(directiveResult.rows.length).toBe(1);
expect(directiveResult.rows[0]?.steward?.email).toBe(
"test+1@permanent.org"
);
expect(directiveResult.rows[0]?.note).toBe(testNote);
expect(directiveResult.rows[0]?.type).toBe("transfer");

expect(sendArchiveStewardNotification).toHaveBeenCalledTimes(0);
});

test("should log error if notification email fails", async () => {
const testError = new Error("out of cheese error - redo from start");
(
sendArchiveStewardNotification as jest.MockedFunction<
typeof sendArchiveStewardNotification
>
).mockRejectedValueOnce(testError);
await directiveService.updateDirective(testDirectiveId, {
emailFromAuthToken: "test@permanent.org",
stewardEmail: "test+2@permanent.org",
note: testNote,
});

expect(sendArchiveStewardNotification).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith(testError);
});

test("should error if authenticated account doesn't own the directive", async () => {
Expand Down
33 changes: 22 additions & 11 deletions src/directive/service/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
isMissingStewardAccountError,
getInvalidValueFromInvalidEnumMessage,
} from "../../database_util";
import { sendArchiveStewardNotification } from "../../email";
import { logger } from "../../log";
import type {
UpdateDirectiveRequest,
Expand All @@ -27,18 +28,19 @@ export const updateDirective = async (
throw new createError.NotFound("Directive not found");
}

const directiveToReturn = await db.transaction(async (transactionDb) => {
const directive = await (async (): Promise<Directive> => {
const directiveData = await db.transaction(async (transactionDb) => {
const directive = await (async (): Promise<
Directive & { stewardChanged: boolean }
> => {
try {
const directiveResult = await transactionDb.sql<Directive>(
"directive.queries.update_directive",
{
directiveId,
type: requestBody.type,
stewardEmail: requestBody.stewardEmail,
note: requestBody.note,
}
);
const directiveResult = await transactionDb.sql<
Directive & { stewardChanged: boolean }
>("directive.queries.update_directive", {
directiveId,
type: requestBody.type,
stewardEmail: requestBody.stewardEmail,
note: requestBody.note,
});
if (directiveResult.rows[0] === undefined) {
throw new createError.BadRequest(
"Directives cannot be updated after they've been executed"
Expand Down Expand Up @@ -95,5 +97,14 @@ export const updateDirective = async (
return directive;
});

const { stewardChanged, ...directiveToReturn } = directiveData;
if (stewardChanged) {
await sendArchiveStewardNotification(directiveToReturn.directiveId).catch(
(err) => {
logger.error(err);
}
);
}

return directiveToReturn;
};
7 changes: 5 additions & 2 deletions src/email/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { sendLegacyContactNotification } from "./service";
import {
sendLegacyContactNotification,
sendArchiveStewardNotification,
} from "./service";

export { sendLegacyContactNotification };
export { sendLegacyContactNotification, sendArchiveStewardNotification };
25 changes: 25 additions & 0 deletions src/email/queries/get_archive_stewardship_details.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
SELECT
steward_account.fullName AS "stewardName",
steward_account.primaryEmail AS "stewardEmail",
owner_account.fullName AS "ownerName",
profile_item.string1 AS "archiveName"
FROM
directive
JOIN
account AS steward_account
ON steward_account.accountId = directive.steward_account_id
JOIN
account_archive
ON account_archive.archiveId = directive.archive_id
AND account_archive.accessRole = 'access.role.owner'
AND account_archive.status = 'status.generic.ok'
JOIN
account AS owner_account
ON owner_account.accountId = account_archive.accountId
JOIN
profile_item
ON profile_item.archiveId = directive.archive_id
AND profile_item.fieldNameUI = 'profile.basic'
AND profile_item.status != 'status.generic.deleted'
WHERE
directive.directive_id = :directiveId
Loading

0 comments on commit 6000d66

Please sign in to comment.