Skip to content

Commit

Permalink
chore: email notification when latest sow upload
Browse files Browse the repository at this point in the history
x
  • Loading branch information
RRanath authored and ccbc-service-account committed Oct 17, 2024
1 parent 9994004 commit bf25ab4
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 6 deletions.
9 changes: 9 additions & 0 deletions app/backend/lib/emails/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import rfiCoverageMapKmzUploaded from './templates/rfiCoverageMapKmzUploaded';
import notifyConditionallyApproved from './templates/notifyConditionallyApproved';
import notifyApplicationSubmission from './templates/notifyApplicationSubmission';
import notifyFailedReadOfTemplateData from './templates/notifyFailedReadOfTemplateData';
import notifySowUpload from './templates/notifySowUpload';

const email = Router();

Expand Down Expand Up @@ -89,4 +90,12 @@ email.post('/api/email/notifyFailedReadOfTemplateData', limiter, (req, res) => {
);
});

email.post('/api/email/notifySowUpload', limiter, (req, res) => {
const { params, ccbcNumber } = req.body;
return handleEmailNotification(req, res, notifySowUpload, {
...params,
ccbcNumber,
});
});

export default email;
31 changes: 31 additions & 0 deletions app/backend/lib/emails/templates/notifySowUpload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
EmailTemplate,
EmailTemplateProvider,
} from '../handleEmailNotification';

const notifySowUpload: EmailTemplateProvider = (
applicationId: string,
url: string,
initiator: any,
params: any
): EmailTemplate => {
const { ccbcNumber, amendmentNumber } = params;
const amendmentLink = `<a href='${url}/analyst/application/${applicationId}/project?section=projectInformation'>${amendmentNumber}</a>`;
const description = amendmentNumber
? `Amendment ${amendmentLink} for ${ccbcNumber} due to a change request.`
: `${ccbcNumber}.`;
return {
emailTo: [70],
emailCC: [],
tag: 'sow-upload-review',
subject: `Action Required - Review Project Description and Project Type for ${ccbcNumber}`,
body: `
<h1>${initiator.givenName} has uploaded a SOW for ${description}</h1>
<p>Please review the Project Description and Project Type in the header <a href='${url}/analyst/application/${applicationId}/summary'>here</a> and update if required.</p>
<p>To unsubscribe from this notification please forward this email with your request to <a href="mailto:meherzad.romer@gov.bc.ca">meherzad.romer@gov.bc.ca<a/></p>
`,
};
};

export default notifySowUpload;
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Toast from 'components/Toast';
import Ajv8Validator from '@rjsf/validator-ajv8';
import excelValidateGenerator from 'lib/helpers/excelValidate';
import ReadOnlyView from 'components/Analyst/Project/ProjectInformation/ReadOnlyView';
import * as Sentry from '@sentry/nextjs';
import ChangeRequestTheme from '../ChangeRequestTheme';

const StyledProjectForm = styled(ProjectForm)`
Expand Down Expand Up @@ -201,6 +202,27 @@ const ProjectInformationForm: React.FC<Props> = ({
setIsSubmitAttempted(false);
};

const notifySowUpload = (params = {}) => {
fetch('/api/email/notifySowUpload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
applicationId: rowId,
host: window.location.origin,
ccbcNumber,
params,
}),
}).then((response) => {
if (!response.ok) {
Sentry.captureException({
name: 'Error sending email to notify SOW upload',
message: response,
});
}
return response.json();
});
};

const handleSubmit = (e) => {
e.preventDefault();
setIsSubmitAttempted(true);
Expand Down Expand Up @@ -230,6 +252,8 @@ const ProjectInformationForm: React.FC<Props> = ({
hasFormErrors &&
formData.hasFundingAgreementBeenSigned;

const latestAmendment = changeRequestData?.[0].node?.amendmentNumber;

const isChangeRequestFormInvalid =
isChangeRequest && (hasFormErrors || !isAmendmentValid);

Expand Down Expand Up @@ -278,6 +302,13 @@ const ProjectInformationForm: React.FC<Props> = ({
handleResetFormData();

if (isSowUploaded && response?.status === 200) {
// send email notification to notify the analyst that the sow has been uploaded
// if this is the latest SOW
if (latestAmendment <= changeRequestAmendmentNumber) {
notifySowUpload({
amendmentNumber: changeRequestAmendmentNumber,
});
}
setShowToast(true);
}

Expand Down Expand Up @@ -332,6 +363,7 @@ const ProjectInformationForm: React.FC<Props> = ({
handleResetFormData(!formData?.hasFundingAgreementBeenSigned);
setHasFormSaved(true);
if (isSowUploaded && response?.status === 200) {
if (!latestAmendment) notifySowUpload();
setShowToast(true);
}
},
Expand Down
30 changes: 25 additions & 5 deletions app/pages/analyst/application/[applicationId]/project.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { usePreloadedQuery } from 'react-relay/hooks';
import { withRelay, RelayProps } from 'relay-nextjs';
import { graphql } from 'react-relay';
Expand All @@ -21,6 +21,7 @@ import {
isMilestonesOpen,
isConditionalApprovalComplete,
} from 'utils/projectAccordionValidators';
import { useRouter } from 'next/router';

const getProjectQuery = graphql`
query projectQuery($rowId: Int!) {
Expand Down Expand Up @@ -75,6 +76,23 @@ const Project = ({
const [isCommunityProgressExpanded, setIsCommunityProgressExpanded] =
useState(false);

const { section: toggledSection } = useRouter().query;
const projectInformationRef = useRef(null);

useEffect(() => {
const sectionRefs = {
projectInformation: projectInformationRef,
};
const anchorRef = sectionRefs[toggledSection as string];
if (toggledSection && anchorRef?.current) {
window.scrollTo({
top:
anchorRef.current.getBoundingClientRect().top + window.scrollY + 100,
behavior: 'smooth',
});
}
}, [toggledSection]);

useEffect(() => {
const isFundingAgreementSigned =
projectInformation?.jsonData?.hasFundingAgreementBeenSigned;
Expand Down Expand Up @@ -118,10 +136,12 @@ const Project = ({
)}
{showAnnouncement && <AnnouncementsForm query={query} />}
{showProjectInformation && (
<ProjectInformationForm
application={applicationByRowId}
isExpanded={isProjectInformationExpanded}
/>
<div ref={projectInformationRef}>
<ProjectInformationForm
application={applicationByRowId}
isExpanded={isProjectInformationExpanded}
/>
</div>
)}
{showCommunityProgressReport && (
<CommunityProgressReportForm
Expand Down
16 changes: 16 additions & 0 deletions app/tests/backend/lib/emails/email.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import rfiCoverageMapKmzUploaded from 'backend/lib/emails/templates/rfiCoverageM
import notifyConditionallyApproved from 'backend/lib/emails/templates/notifyConditionallyApproved';
import notifyApplicationSubmission from 'backend/lib/emails/templates/notifyApplicationSubmission';
import notifyFailedReadOfTemplateData from 'backend/lib/emails/templates/notifyFailedReadOfTemplateData';
import notifySowUpload from 'backend/lib/emails/templates/notifySowUpload';

jest.mock('backend/lib/emails/handleEmailNotification');

Expand Down Expand Up @@ -227,4 +228,19 @@ describe('Email API Endpoints', () => {
{}
);
});

it('calls notifySowUpload with correct parameters once notifySowUpload called', async () => {
const reqBody = {
applicationId: '',
ccbcNumber: 'CCBC-00001',
params: { amendmentNumber: 1 },
};
await request(app).post('/api/email/notifySowUpload').send(reqBody);
expect(handleEmailNotification).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
notifySowUpload,
{ ccbcNumber: 'CCBC-00001', amendmentNumber: 1 }
);
});
});
65 changes: 65 additions & 0 deletions app/tests/backend/lib/emails/templates/notifySowUpload.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import notifySowUpload from 'backend/lib/emails/templates/notifySowUpload';

describe('notifySowUpload template', () => {
it('should return an email template with correct properties', () => {
const applicationId = '1';
const url = 'http://mock_host.ca';

const emailTemplate = notifySowUpload(
applicationId,
url,
{},
{ ccbcNumber: 'CCBC-101', amendmentNumber: 1 }
);

expect(emailTemplate).toEqual(
expect.objectContaining({
emailTo: [70],
emailCC: [],
tag: 'sow-upload-review',
subject:
'Action Required - Review Project Description and Project Type for CCBC-101',
body: expect.anything(),
})
);
});

it('should include correct URL in the body for original sow upload', () => {
const applicationId = '1';
const url = 'http://mock_host.ca';

const emailTemplate: any = notifySowUpload(
applicationId,
url,
{ givenName: 'uniqueStringName' },
{ ccbcNumber: 'CCBC-101' }
);

expect(emailTemplate.body).toContain(
`<a href='http://mock_host.ca/analyst/application/1/summary'>here</a>`
);

expect(emailTemplate.body).toContain('uniqueStringName');
});

it('should include correct URL in the body for amendment sow upload', () => {
const applicationId = '1';
const url = 'http://mock_host.ca';

const emailTemplate: any = notifySowUpload(
applicationId,
url,
{ givenName: 'uniqueStringName' },
{ ccbcNumber: 'CCBC-101', amendmentNumber: 1 }
);

expect(emailTemplate.body).toContain(
`<a href='http://mock_host.ca/analyst/application/1/summary'>here</a>`
);
expect(emailTemplate.body).toContain(
`<a href='http://mock_host.ca/analyst/application/1/project?section=projectInformation'>1</a>`
);

expect(emailTemplate.body).toContain('uniqueStringName');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ describe('The ProjectInformation form', () => {
).toBeInTheDocument();
});

it('calls the mutation on Change Request save', async () => {
it('calls the mutation on Change Request save and send email notification for SOW upload', async () => {
componentTestingHelper.loadQuery(mockDataQueryPayload);
componentTestingHelper.renderComponent();

Expand Down Expand Up @@ -406,6 +406,14 @@ describe('The ProjectInformation form', () => {
expect(
screen.getByText('Statement of work successfully imported')
).toBeInTheDocument();

expect(global.fetch).toHaveBeenCalledWith('/api/email/notifySowUpload', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: expect.anything(),
});
});

it('should show a spinner when the sow is being imported', async () => {
Expand Down

0 comments on commit bf25ab4

Please sign in to comment.