From e81d1323535d539de9c6fa4360d8f52c91808ed2 Mon Sep 17 00:00:00 2001 From: R Ranathunga Date: Thu, 26 Sep 2024 07:29:37 -0700 Subject: [PATCH] chore: email notification when latest sow upload x --- app/backend/lib/emails/email.ts | 9 +++ .../lib/emails/templates/notifySowUpload.ts | 31 +++++++++ .../ProjectInformationForm.tsx | 32 +++++++++ .../application/[applicationId]/project.tsx | 30 +++++++-- app/tests/backend/lib/emails/email.test.ts | 16 +++++ .../emails/templates/notifySowUpload.test.ts | 65 +++++++++++++++++++ .../ProjectInformationForm.test.ts | 10 ++- 7 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 app/backend/lib/emails/templates/notifySowUpload.ts create mode 100644 app/tests/backend/lib/emails/templates/notifySowUpload.test.ts diff --git a/app/backend/lib/emails/email.ts b/app/backend/lib/emails/email.ts index d871781e9d..50661c9c02 100644 --- a/app/backend/lib/emails/email.ts +++ b/app/backend/lib/emails/email.ts @@ -9,6 +9,7 @@ import householdCountUpdate from './templates/householdCountUpdate'; import rfiCoverageMapKmzUploaded from './templates/rfiCoverageMapKmzUploaded'; import notifyConditionallyApproved from './templates/notifyConditionallyApproved'; import notifyApplicationSubmission from './templates/notifyApplicationSubmission'; +import notifySowUpload from './templates/notifySowUpload'; const email = Router(); @@ -78,4 +79,12 @@ email.post('/api/email/notifyApplicationSubmission', 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; diff --git a/app/backend/lib/emails/templates/notifySowUpload.ts b/app/backend/lib/emails/templates/notifySowUpload.ts new file mode 100644 index 0000000000..0c80e4b406 --- /dev/null +++ b/app/backend/lib/emails/templates/notifySowUpload.ts @@ -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 = `${amendmentNumber}`; + 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: ` +

${initiator.givenName} has uploaded a SOW for ${description}

+

Please review the Project Description and Project Type in the header here and update if required.

+ +

To unsubscribe from this notification please forward this email with your request to meherzad.romer@gov.bc.ca

+ `, + }; +}; + +export default notifySowUpload; diff --git a/app/components/Analyst/Project/ProjectInformation/ProjectInformationForm.tsx b/app/components/Analyst/Project/ProjectInformation/ProjectInformationForm.tsx index e861b59c6c..17b599dd32 100644 --- a/app/components/Analyst/Project/ProjectInformation/ProjectInformationForm.tsx +++ b/app/components/Analyst/Project/ProjectInformation/ProjectInformationForm.tsx @@ -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)` @@ -201,6 +202,27 @@ const ProjectInformationForm: React.FC = ({ 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); @@ -230,6 +252,8 @@ const ProjectInformationForm: React.FC = ({ hasFormErrors && formData.hasFundingAgreementBeenSigned; + const latestAmendment = changeRequestData?.[0].node?.amendmentNumber; + const isChangeRequestFormInvalid = isChangeRequest && (hasFormErrors || !isAmendmentValid); @@ -278,6 +302,13 @@ const ProjectInformationForm: React.FC = ({ 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); } @@ -332,6 +363,7 @@ const ProjectInformationForm: React.FC = ({ handleResetFormData(!formData?.hasFundingAgreementBeenSigned); setHasFormSaved(true); if (isSowUploaded && response?.status === 200) { + if (!latestAmendment) notifySowUpload(); setShowToast(true); } }, diff --git a/app/pages/analyst/application/[applicationId]/project.tsx b/app/pages/analyst/application/[applicationId]/project.tsx index 5fa5e02f04..01d63cb4f3 100644 --- a/app/pages/analyst/application/[applicationId]/project.tsx +++ b/app/pages/analyst/application/[applicationId]/project.tsx @@ -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'; @@ -21,6 +21,7 @@ import { isMilestonesOpen, isConditionalApprovalComplete, } from 'utils/projectAccordionValidators'; +import { useRouter } from 'next/router'; const getProjectQuery = graphql` query projectQuery($rowId: Int!) { @@ -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; @@ -118,10 +136,12 @@ const Project = ({ )} {showAnnouncement && } {showProjectInformation && ( - +
+ +
)} {showCommunityProgressReport && ( { {} ); }); + + 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 } + ); + }); }); diff --git a/app/tests/backend/lib/emails/templates/notifySowUpload.test.ts b/app/tests/backend/lib/emails/templates/notifySowUpload.test.ts new file mode 100644 index 0000000000..ac32430308 --- /dev/null +++ b/app/tests/backend/lib/emails/templates/notifySowUpload.test.ts @@ -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( + `
here` + ); + + 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( + `here` + ); + expect(emailTemplate.body).toContain( + `1` + ); + + expect(emailTemplate.body).toContain('uniqueStringName'); + }); +}); diff --git a/app/tests/components/Analyst/Project/ProjectInformation/ProjectInformationForm.test.ts b/app/tests/components/Analyst/Project/ProjectInformation/ProjectInformationForm.test.ts index 8a696ef24e..2161012d3e 100644 --- a/app/tests/components/Analyst/Project/ProjectInformation/ProjectInformationForm.test.ts +++ b/app/tests/components/Analyst/Project/ProjectInformation/ProjectInformationForm.test.ts @@ -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(); @@ -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 () => {