diff --git a/app/backend/lib/ches/sendEmail.ts b/app/backend/lib/ches/sendEmail.ts index 1b8fc44369..a8e9656680 100644 --- a/app/backend/lib/ches/sendEmail.ts +++ b/app/backend/lib/ches/sendEmail.ts @@ -1,7 +1,10 @@ import * as Sentry from '@sentry/nextjs'; +import getConfig from 'next/config'; +import toTitleCase from '../../../utils/formatString'; import config from '../../../config'; const CHES_API_URL = config.get('CHES_API_URL'); +const namespace = getConfig()?.publicRuntimeConfig?.OPENSHIFT_APP_NAMESPACE; const sendEmail = async ( token: string, @@ -11,6 +14,7 @@ const sendEmail = async ( tag: string, emailCC: string[] = [] ) => { + const environment = toTitleCase(namespace?.split('-')[1] || 'Dev'); try { const request = { bodyType: 'html', @@ -18,7 +22,7 @@ const sendEmail = async ( cc: emailCC, delayTs: 0, encoding: 'utf-8', - from: 'CCBC Portal ', + from: `CCBC Portal ${environment !== 'Prod' && environment} `, priority: 'normal', subject, to: emailTo, diff --git a/app/backend/lib/ches/sendEmailMerge.ts b/app/backend/lib/ches/sendEmailMerge.ts index 2ba020889c..fac9b0bb24 100644 --- a/app/backend/lib/ches/sendEmailMerge.ts +++ b/app/backend/lib/ches/sendEmailMerge.ts @@ -1,7 +1,10 @@ import * as Sentry from '@sentry/nextjs'; +import getConfig from 'next/config'; +import toTitleCase from '../../../utils/formatString'; import config from '../../../config'; const CHES_API_URL = config.get('CHES_API_URL'); +const namespace = getConfig()?.publicRuntimeConfig?.OPENSHIFT_APP_NAMESPACE; export interface Context { to: string[]; @@ -19,13 +22,14 @@ const sendEmailMerge = async ( subject: string, contexts: Contexts ) => { + const environment = toTitleCase(namespace?.split('-')[1] || 'Dev'); try { const request = { bodyType: 'html', body, contexts, encoding: 'utf-8', - from: 'CCBC Portal ', + from: `CCBC Portal ${environment !== 'Prod' && environment} `, priority: 'normal', subject, attachments: [], diff --git a/app/backend/lib/emails/email.ts b/app/backend/lib/emails/email.ts index 67ef724960..2742ed6604 100644 --- a/app/backend/lib/emails/email.ts +++ b/app/backend/lib/emails/email.ts @@ -11,6 +11,7 @@ import notifyConditionallyApproved from './templates/notifyConditionallyApproved import notifyApplicationSubmission from './templates/notifyApplicationSubmission'; import notifyFailedReadOfTemplateData from './templates/notifyFailedReadOfTemplateData'; import notifySowUpload from './templates/notifySowUpload'; +import notifyDocumentUpload from './templates/notifyDocumentUpload'; const email = Router(); @@ -98,4 +99,11 @@ email.post('/api/email/notifySowUpload', limiter, (req, res) => { }); }); +email.post('/api/email/notifyDocumentUpload', limiter, (req, res) => { + const { params } = req.body; + return handleEmailNotification(req, res, notifyDocumentUpload, { + ...params, + }); +}); + export default email; diff --git a/app/backend/lib/emails/templates/notifyDocumentUpload.ts b/app/backend/lib/emails/templates/notifyDocumentUpload.ts new file mode 100644 index 0000000000..2c2ae4b74b --- /dev/null +++ b/app/backend/lib/emails/templates/notifyDocumentUpload.ts @@ -0,0 +1,38 @@ +import { + EmailTemplate, + EmailTemplateProvider, +} from '../handleEmailNotification'; + +const notifyDocumentUpload: EmailTemplateProvider = ( + applicationId: string, + url: string, + initiator: any, + params: any +): EmailTemplate => { + const { ccbcNumber, documentType, timestamp, documentNames } = params; + + const section = { + 'Claim & Progress Report': 'project?section=claimsReport', + 'Community Progress Report': 'project?section=communityProgressReport', + 'Milestone Report': 'project?section=milestoneReport', + 'Statement of Work': 'project?section=projectInformation', + }; + + const link = `${ccbcNumber}`; + return { + emailTo: [112, 10, 111], + emailCC: [], + tag: 'document-upload-notification', + subject: `${documentType} uploaded in Portal`, + body: ` +

${documentType} uploaded in Portal

+ +

Notification: A ${documentType} has been uploaded in the Portal for ${link} on ${timestamp}.

+
    + ${documentNames.map((file) => `
  • ${file}
  • `).join('')} +
+ `, + }; +}; + +export default notifyDocumentUpload; diff --git a/app/components/Analyst/History/HistoryFilter.tsx b/app/components/Analyst/History/HistoryFilter.tsx index 8ed63a15d8..8c136dfa4a 100644 --- a/app/components/Analyst/History/HistoryFilter.tsx +++ b/app/components/Analyst/History/HistoryFilter.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { FormBase } from 'components/Form'; import { historyFilter } from 'formSchema/analyst'; import historyFilterUiSchema from 'formSchema/uiSchema/history/historyFilterUiSchema'; -import transformToTitleCase from 'utils/formatString'; +import toTitleCase from 'utils/formatString'; interface HistoryFilterProps { filterOptions: { typeOptions: string[]; userOptions: string[] }; @@ -53,7 +53,7 @@ const HistoryFilter: React.FC = ({ .filter((type) => type !== 'attachment') .map((type) => ({ value: type, - label: transformToTitleCase(type), + label: toTitleCase(type, '_'), })); const filterSchema = historyFilter(formattedTypeOptions, userOptions); diff --git a/app/components/Analyst/Project/Claims/ClaimsForm.tsx b/app/components/Analyst/Project/Claims/ClaimsForm.tsx index 9265440f30..16fdaa8853 100644 --- a/app/components/Analyst/Project/Claims/ClaimsForm.tsx +++ b/app/components/Analyst/Project/Claims/ClaimsForm.tsx @@ -8,6 +8,7 @@ import { useArchiveApplicationClaimsDataMutation as useArchiveClaims } from 'sch import excelValidateGenerator from 'lib/helpers/excelValidate'; import Toast from 'components/Toast'; import useModal from 'lib/helpers/useModal'; +import useEmailNotification from 'lib/helpers/useEmailNotification'; import ClaimsView from './ClaimsView'; import ProjectTheme from '../ProjectTheme'; import ProjectForm from '../ProjectForm'; @@ -105,6 +106,7 @@ const ClaimsForm: React.FC = ({ application, isExpanded }) => { const [isFormEditMode, setIsFormEditMode] = useState(false); const [createClaims] = useCreateClaimsMutation(); const [archiveClaims] = useArchiveClaims(); + const { notifyDocumentUpload } = useEmailNotification(); const hiddenSubmitRef = useRef(null); // use this to live validate the form after the first submit attempt const [isSubmitAttempted, setIsSubmitAttempted] = useState(false); @@ -195,6 +197,11 @@ const ClaimsForm: React.FC = ({ application, isExpanded }) => { if (res?.status === 200) { setShowToast(true); + notifyDocumentUpload(applicationRowId, { + documentType: 'Claim & Progress Report', + ccbcNumber, + documentNames: [excelFile.name], + }); } }, onError: () => { diff --git a/app/components/Analyst/Project/CommunityProgressReport/CommunityProgressReportForm.tsx b/app/components/Analyst/Project/CommunityProgressReport/CommunityProgressReportForm.tsx index 9f8995ad71..9f5dec308d 100644 --- a/app/components/Analyst/Project/CommunityProgressReport/CommunityProgressReportForm.tsx +++ b/app/components/Analyst/Project/CommunityProgressReport/CommunityProgressReportForm.tsx @@ -9,6 +9,7 @@ import excelValidateGenerator from 'lib/helpers/excelValidate'; import { getFiscalQuarter, getFiscalYear } from 'utils/fiscalFormat'; import Toast from 'components/Toast'; import useModal from 'lib/helpers/useModal'; +import useEmailNotification from 'lib/helpers/useEmailNotification'; import CommunityProgressView from './CommunityProgressView'; import ProjectTheme from '../ProjectTheme'; import ProjectForm from '../ProjectForm'; @@ -108,6 +109,7 @@ const CommunityProgressReportForm: React.FC = ({ const [createCommunityProgressReport] = useCreateCommunityProgressReportMutation(); const [archiveCommunityProgressReport] = useArchiveCpr(); + const { notifyDocumentUpload } = useEmailNotification(); const hiddenSubmitRef = useRef(null); // use this to live validate the form after the first submit attempt const [isSubmitAttempted, setIsSubmitAttempted] = useState(false); @@ -207,6 +209,11 @@ const CommunityProgressReportForm: React.FC = ({ if (res?.status === 200) { setShowToast(true); + notifyDocumentUpload(applicationRowId, { + ccbcNumber, + documentType: 'Community Progress Report', + documentNames: [excelFile.name], + }); } }, onError: () => { diff --git a/app/components/Analyst/Project/Milestones/MilestonesForm.tsx b/app/components/Analyst/Project/Milestones/MilestonesForm.tsx index 9d7d13711c..203b8bbd41 100644 --- a/app/components/Analyst/Project/Milestones/MilestonesForm.tsx +++ b/app/components/Analyst/Project/Milestones/MilestonesForm.tsx @@ -8,6 +8,7 @@ import { useArchiveApplicationMilestoneDataMutation as useArchiveMilestone } fro import excelValidateGenerator from 'lib/helpers/excelValidate'; import Toast from 'components/Toast'; import useModal from 'lib/helpers/useModal'; +import useEmailNotification from 'lib/helpers/useEmailNotification'; import MilestonesView from './MilestonesView'; import ProjectTheme from '../ProjectTheme'; import ProjectForm from '../ProjectForm'; @@ -125,6 +126,7 @@ const MilestonesForm: React.FC = ({ application, isExpanded }) => { ccbcNumber, } = queryFragment; + const { notifyDocumentUpload } = useEmailNotification(); const [formData, setFormData] = useState({} as FormData); const deleteConfirmationModal = useModal(); // store the current community progress data node for edit mode so we have access to row id and relay connection @@ -226,6 +228,11 @@ const MilestonesForm: React.FC = ({ application, isExpanded }) => { if (res?.status === 200) { setShowToast(true); + notifyDocumentUpload(applicationRowId, { + documentType: 'Milestone Report', + documentNames: [excelFile.name], + ccbcNumber, + }); } }, onError: () => { diff --git a/app/components/Analyst/Project/ProjectInformation/ProjectInformationForm.tsx b/app/components/Analyst/Project/ProjectInformation/ProjectInformationForm.tsx index 8b2a5e8b79..e20c2a7c16 100644 --- a/app/components/Analyst/Project/ProjectInformation/ProjectInformationForm.tsx +++ b/app/components/Analyst/Project/ProjectInformation/ProjectInformationForm.tsx @@ -16,6 +16,7 @@ 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 useEmailNotification from 'lib/helpers/useEmailNotification'; import ChangeRequestTheme from '../ChangeRequestTheme'; const StyledProjectForm = styled(ProjectForm)` @@ -96,6 +97,7 @@ const ProjectInformationForm: React.FC = ({ const [createProjectInformation] = useCreateProjectInformationMutation(); const [archiveApplicationSow] = useArchiveApplicationSowMutation(); const [createChangeRequest] = useCreateChangeRequestMutation(); + const { notifyDocumentUpload } = useEmailNotification(); const [hasFormSaved, setHasFormSaved] = useState(false); const [formData, setFormData] = useState(projectInformation?.jsonData); const [showToast, setShowToast] = useState(false); @@ -221,6 +223,12 @@ const ProjectInformationForm: React.FC = ({ } return response.json(); }); + + notifyDocumentUpload(rowId, { + ccbcNumber, + documentType: 'Statement of Work', + documentNames: [sowFile.name], + }); }; const handleSubmit = (e) => { diff --git a/app/components/Analyst/RFI/RFIAnalystUpload.tsx b/app/components/Analyst/RFI/RFIAnalystUpload.tsx index c104d7ee7d..22841ba217 100644 --- a/app/components/Analyst/RFI/RFIAnalystUpload.tsx +++ b/app/components/Analyst/RFI/RFIAnalystUpload.tsx @@ -11,10 +11,11 @@ import { useUpdateWithTrackingRfiMutation } from 'schema/mutations/application/u import styled from 'styled-components'; import { useCreateNewFormDataMutation } from 'schema/mutations/application/createNewFormData'; -import useHHCountUpdateEmail from 'lib/helpers/useHHCountUpdateEmail'; +import useEmailNotification from 'lib/helpers/useEmailNotification'; import useRfiCoverageMapKmzUploadedEmail from 'lib/helpers/useRfiCoverageMapKmzUploadedEmail'; import { useToast } from 'components/AppProvider'; import Link from 'next/link'; +import joinWithAnd from 'utils/formatArray'; const Flex = styled('header')` display: flex; @@ -68,14 +69,16 @@ const RfiAnalystUpload = ({ query }) => { const [newFormData, setNewFormData] = useState(jsonData); const [templateData, setTemplateData] = useState(null); const [excelImportFields, setExcelImportFields] = useState([]); + const [excelImportFiles, setExcelImportFiles] = useState([]); const router = useRouter(); - const { notifyHHCountUpdate } = useHHCountUpdateEmail(); + const { notifyHHCountUpdate, notifyDocumentUpload } = useEmailNotification(); const { notifyRfiCoverageMapKmzUploaded } = useRfiCoverageMapKmzUploadedEmail(); useEffect(() => { if (templateData?.templateNumber === 1 && !templateData?.error) { setExcelImportFields([...excelImportFields, 'Template 1']); + setExcelImportFiles([...excelImportFiles, templateData?.templateName]); const newFormDataWithTemplateOne = { ...newFormData, benefits: { @@ -88,6 +91,7 @@ const RfiAnalystUpload = ({ query }) => { setNewFormData(newFormDataWithTemplateOne); } else if (templateData?.templateNumber === 2 && !templateData?.error) { setExcelImportFields([...excelImportFields, 'Template 2']); + setExcelImportFiles([...excelImportFiles, templateData?.templateName]); const newFormDataWithTemplateTwo = { ...newFormData, budgetDetails: { @@ -97,6 +101,9 @@ const RfiAnalystUpload = ({ query }) => { }, }; setNewFormData(newFormDataWithTemplateTwo); + } else if (templateData?.templateNumber === 9 && !templateData?.error) { + setExcelImportFields([...excelImportFields, 'Template 9']); + setExcelImportFiles([...excelImportFiles, templateData?.templateName]); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [templateData]); @@ -163,6 +170,11 @@ const RfiAnalystUpload = ({ query }) => { ) { showToast(getToastMessage(), 'success', 100000000); } + notifyDocumentUpload(applicationId, { + ccbcNumber, + documentType: joinWithAnd(excelImportFields), + documentNames: excelImportFiles, + }); }, }); } diff --git a/app/lib/helpers/useHHCountUpdateEmail.ts b/app/lib/helpers/useEmailNotification.ts similarity index 62% rename from app/lib/helpers/useHHCountUpdateEmail.ts rename to app/lib/helpers/useEmailNotification.ts index 4587d9680f..2c17c8f313 100644 --- a/app/lib/helpers/useHHCountUpdateEmail.ts +++ b/app/lib/helpers/useEmailNotification.ts @@ -1,6 +1,6 @@ import * as Sentry from '@sentry/nextjs'; -const useHHCountUpdateEmail = () => { +const useEmailNotification = () => { const notifyHHCountUpdate = async ( newData: any, oldData: any, @@ -48,7 +48,30 @@ const useHHCountUpdateEmail = () => { }); }; - return { notifyHHCountUpdate }; + const notifyDocumentUpload = async (applicationId: string, params: any) => { + fetch('/api/email/notifyDocumentUpload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + applicationId, + host: window.location.origin, + params: { + ...params, + timestamp: new Date().toLocaleString(), + }, + }), + }).then((response) => { + if (!response.ok) { + Sentry.captureException({ + name: `Error sending email to notify ${params.documentType} upload`, + message: response, + }); + } + return response.json(); + }); + }; + + return { notifyHHCountUpdate, notifyDocumentUpload }; }; -export default useHHCountUpdateEmail; +export default useEmailNotification; diff --git a/app/lib/theme/widgets/FileWidget.tsx b/app/lib/theme/widgets/FileWidget.tsx index e517dfcc8a..ba08de1d72 100644 --- a/app/lib/theme/widgets/FileWidget.tsx +++ b/app/lib/theme/widgets/FileWidget.tsx @@ -100,6 +100,7 @@ const FileWidget: React.FC = ({ setTemplateData({ templateNumber, data, + templateName: file.name, }); } else { isTemplateValid = false; @@ -118,6 +119,10 @@ const FileWidget: React.FC = ({ ); if (response.ok) { await response.json(); + setTemplateData({ + templateNumber, + templateName: file.name, + }); } else { isTemplateValid = false; setTemplateData({ diff --git a/app/pages/analyst/application/[applicationId]/edit/[section].tsx b/app/pages/analyst/application/[applicationId]/edit/[section].tsx index e3ba11f6b8..1e100211a0 100644 --- a/app/pages/analyst/application/[applicationId]/edit/[section].tsx +++ b/app/pages/analyst/application/[applicationId]/edit/[section].tsx @@ -20,7 +20,7 @@ import { useCreateNewFormDataMutation } from 'schema/mutations/application/creat import { analystProjectArea, benefits } from 'formSchema/uiSchema/pages'; import useModal from 'lib/helpers/useModal'; import { RJSFSchema } from '@rjsf/utils'; -import useHHCountUpdateEmail from 'lib/helpers/useHHCountUpdateEmail'; +import useEmailNotification from 'lib/helpers/useEmailNotification'; const getSectionQuery = graphql` query SectionQuery($rowId: Int!) { @@ -85,7 +85,7 @@ const EditApplication = ({ const [changeReason, setChangeReason] = useState(''); const [isFormSaved, setIsFormSaved] = useState(true); const changeModal = useModal(); - const { notifyHHCountUpdate } = useHHCountUpdateEmail(); + const { notifyHHCountUpdate } = useEmailNotification(); const handleChange = (e: IChangeEvent) => { setIsFormSaved(false); const newFormSectionData = { ...e.formData }; diff --git a/app/pages/analyst/application/[applicationId]/project.tsx b/app/pages/analyst/application/[applicationId]/project.tsx index 36c7634ff0..efc0c98b14 100644 --- a/app/pages/analyst/application/[applicationId]/project.tsx +++ b/app/pages/analyst/application/[applicationId]/project.tsx @@ -91,10 +91,16 @@ const Project = ({ const { section: toggledSection } = useRouter().query; const projectInformationRef = useRef(null); + const communityProgressReportRef = useRef(null); + const milestoneRef = useRef(null); + const claimsRef = useRef(null); const sectionRefs = useMemo( () => ({ projectInformation: projectInformationRef, + communityProgressReport: communityProgressReportRef, + milestoneReport: milestoneRef, + claimsReport: claimsRef, }), [] ); @@ -155,7 +161,7 @@ const Project = ({ if (toggledSection === 'conditionalApproval') { setIsConditionalApprovalExpanded(true); } - }, [conditionalApproval, date, projectInformation]); + }, [conditionalApproval, date, projectInformation, toggledSection]); const toggleExpandAll = () => { setIsConditionalApprovalExpanded(true); @@ -225,22 +231,33 @@ const Project = ({ )} {showCommunityProgressReport && ( - +
+ +
)} {showMilestones && ( - +
+ +
)} {showClaims && ( - +
+ +
)} diff --git a/app/pages/applicantportal/form/[id]/rfi/[applicantRfiId].tsx b/app/pages/applicantportal/form/[id]/rfi/[applicantRfiId].tsx index a9ab1116f4..0b95f02931 100644 --- a/app/pages/applicantportal/form/[id]/rfi/[applicantRfiId].tsx +++ b/app/pages/applicantportal/form/[id]/rfi/[applicantRfiId].tsx @@ -16,7 +16,7 @@ import FormDiv from 'components/FormDiv'; import styled from 'styled-components'; import { useEffect, useState } from 'react'; import { useCreateNewFormDataMutation } from 'schema/mutations/application/createNewFormData'; -import useHHCountUpdateEmail from 'lib/helpers/useHHCountUpdateEmail'; +import useEmailNotification from 'lib/helpers/useEmailNotification'; import useRfiCoverageMapKmzUploadedEmail from 'lib/helpers/useRfiCoverageMapKmzUploadedEmail'; const Flex = styled('header')` @@ -73,7 +73,7 @@ const ApplicantRfiPage = ({ const [createNewFormData] = useCreateNewFormDataMutation(); const [templateData, setTemplateData] = useState(null); const [formData, setFormData] = useState(rfiDataByRowId.jsonData); - const { notifyHHCountUpdate } = useHHCountUpdateEmail(); + const { notifyHHCountUpdate } = useEmailNotification(); const { notifyRfiCoverageMapKmzUploaded } = useRfiCoverageMapKmzUploadedEmail(); diff --git a/app/tests/backend/lib/emails/email.test.ts b/app/tests/backend/lib/emails/email.test.ts index 9eafd76c39..5beb85b032 100644 --- a/app/tests/backend/lib/emails/email.test.ts +++ b/app/tests/backend/lib/emails/email.test.ts @@ -17,6 +17,7 @@ import notifyConditionallyApproved from 'backend/lib/emails/templates/notifyCond import notifyApplicationSubmission from 'backend/lib/emails/templates/notifyApplicationSubmission'; import notifyFailedReadOfTemplateData from 'backend/lib/emails/templates/notifyFailedReadOfTemplateData'; import notifySowUpload from 'backend/lib/emails/templates/notifySowUpload'; +import notifyDocumentUpload from 'backend/lib/emails/templates/notifyDocumentUpload'; jest.mock('backend/lib/emails/handleEmailNotification'); @@ -243,4 +244,29 @@ describe('Email API Endpoints', () => { { ccbcNumber: 'CCBC-00001', amendmentNumber: 1 } ); }); + + it('calls notifyDocumentUpload with correct parameters once notifyDocumentUpload called', async () => { + const reqBody = { + applicationId: '1', + ccbcNumber: 'CCBC-00001', + params: { + ccbcNumber: 'CCBC-00001', + documentType: 'Statement of Work', + timestamp: '2024-06-26', + documentNames: ['file1', 'file2'], + }, + }; + await request(app).post('/api/email/notifyDocumentUpload').send(reqBody); + expect(handleEmailNotification).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + notifyDocumentUpload, + { + ccbcNumber: 'CCBC-00001', + documentType: 'Statement of Work', + timestamp: '2024-06-26', + documentNames: ['file1', 'file2'], + } + ); + }); }); diff --git a/app/tests/backend/lib/emails/templates/notifyDocumentUpload.test.ts b/app/tests/backend/lib/emails/templates/notifyDocumentUpload.test.ts new file mode 100644 index 0000000000..fa5379b539 --- /dev/null +++ b/app/tests/backend/lib/emails/templates/notifyDocumentUpload.test.ts @@ -0,0 +1,115 @@ +import notifyDocumentUpload from 'backend/lib/emails/templates/notifyDocumentUpload'; + +describe('notifyDocumentUpload template', () => { + it('should return an email template with correct properties', () => { + const applicationId = '1'; + const url = 'http://mock_host.ca'; + + const emailTemplate = notifyDocumentUpload( + applicationId, + url, + {}, + { + ccbcNumber: 'CCBC-101', + documentType: 'Claim & Progress Report', + documentNames: ['sow.xls'], + timeStamp: '2024/08/24 11:00:00', + } + ); + + expect(emailTemplate).toEqual( + expect.objectContaining({ + emailTo: [112, 10, 111], + emailCC: [], + tag: 'document-upload-notification', + subject: 'Claim & Progress Report uploaded in Portal', + body: expect.anything(), + }) + ); + }); + + it('should generates the correct body and subject based on the document type provided', () => { + const applicationId = '1'; + const url = 'http://mock_host.ca'; + + const emailTemplateClaims: any = notifyDocumentUpload( + applicationId, + url, + {}, + { + ccbcNumber: 'CCBC-10001', + documentType: 'Claim & Progress Report', + documentNames: ['sow.xls'], + timeStamp: '2024/08/24 11:00:00', + } + ); + expect(emailTemplateClaims.body).toContain( + `CCBC-10001` + ); + + const emailTemplateMilestone: any = notifyDocumentUpload( + applicationId, + url, + {}, + { + ccbcNumber: 'CCBC-10001', + documentType: 'Milestone Report', + documentNames: ['milestone.xls'], + timeStamp: '2024/08/24 11:00:00', + } + ); + expect(emailTemplateMilestone.body).toContain( + `CCBC-10001` + ); + + const emailTemplateCommunityProgress: any = notifyDocumentUpload( + applicationId, + url, + {}, + { + ccbcNumber: 'CCBC-10001', + documentType: 'Community Progress Report', + documentNames: ['communityProgress.xls'], + timeStamp: '2024/08/24 11:00:00', + } + ); + expect(emailTemplateCommunityProgress.body).toContain( + `CCBC-10001` + ); + + const emailTemplateCommunitySOW: any = notifyDocumentUpload( + applicationId, + url, + {}, + { + ccbcNumber: 'CCBC-10001', + documentType: 'Statement of Work', + documentNames: ['sow.xls'], + timeStamp: '2024/08/24 11:00:00', + } + ); + expect(emailTemplateCommunitySOW.body).toContain( + `CCBC-10001` + ); + }); + + it('should contain the correct list of filenames', () => { + const applicationId = '1'; + const url = 'http://mock_host.ca'; + + const emailTemplate: any = notifyDocumentUpload( + applicationId, + url, + {}, + { + ccbcNumber: 'CCBC-10001', + documentType: 'Template 1, Template 2 and Template 9', + documentNames: ['template_1.xls', 'template_2.xls', 'template_9.xls'], + timeStamp: '2024/08/24 11:00:00', + } + ); + expect(emailTemplate.body).toContain( + `
  • template_1.xls
  • template_2.xls
  • template_9.xls
  • ` + ); + }); +}); diff --git a/app/tests/backend/lib/helpers/useHHCountUpdateEmail.test.ts b/app/tests/backend/lib/helpers/useEmailNotification.test.ts similarity index 53% rename from app/tests/backend/lib/helpers/useHHCountUpdateEmail.test.ts rename to app/tests/backend/lib/helpers/useEmailNotification.test.ts index a7e785974d..073bd62518 100644 --- a/app/tests/backend/lib/helpers/useHHCountUpdateEmail.test.ts +++ b/app/tests/backend/lib/helpers/useEmailNotification.test.ts @@ -1,5 +1,5 @@ import { renderHook } from '@testing-library/react-hooks'; -import useHHCountUpdateEmail from 'lib/helpers/useHHCountUpdateEmail'; +import useEmailNotification from 'lib/helpers/useEmailNotification'; import * as Sentry from '@sentry/nextjs'; jest.mock('@sentry/nextjs'); @@ -9,13 +9,13 @@ const mockResponse = { }; global.fetch = jest.fn().mockResolvedValue(mockResponse); -describe('useHHCountUpdateEmail', () => { +describe('notifyHHCountUpdate', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should not call fetch if no fields changed', async () => { - const { result } = renderHook(() => useHHCountUpdateEmail()); + const { result } = renderHook(() => useEmailNotification()); await result.current.notifyHHCountUpdate( { numberOfHouseholds: 10, householdsImpactedIndigenous: 5 }, @@ -28,7 +28,7 @@ describe('useHHCountUpdateEmail', () => { }); it('should call email notification if fields have changed', async () => { - const { result } = renderHook(() => useHHCountUpdateEmail()); + const { result } = renderHook(() => useEmailNotification()); await result.current.notifyHHCountUpdate( { numberOfHouseholds: 10, householdsImpactedIndigenous: 5 }, @@ -60,7 +60,7 @@ describe('useHHCountUpdateEmail', () => { }; global.fetch = jest.fn().mockResolvedValueOnce(mockResponseFail); - const { result } = renderHook(() => useHHCountUpdateEmail()); + const { result } = renderHook(() => useEmailNotification()); await result.current.notifyHHCountUpdate( { numberOfHouseholds: 10, householdsImpactedIndigenous: 5 }, @@ -77,3 +77,54 @@ describe('useHHCountUpdateEmail', () => { ); }); }); + +describe('notifyDocumentUpload', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call email notification when called', async () => { + const mockResponseSuccess = { + ok: true, + json: async () => ({}), + }; + global.fetch = jest.fn().mockResolvedValueOnce(mockResponseSuccess); + + const { result } = renderHook(() => useEmailNotification()); + + await result.current.notifyDocumentUpload('12345', { + ccbcNumber: 'CCBC-10001', + documentType: 'Claim & Progress Report', + documentNames: ['sow.xls'], + }); + + expect(fetch).toHaveBeenCalledWith('/api/email/notifyDocumentUpload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: expect.anything(), + }); + }); + + it('should call Sentry.captureException if fetch fails', async () => { + const mockResponseFail = { + ok: false, + json: jest.fn().mockResolvedValue({}), + }; + global.fetch = jest.fn().mockResolvedValueOnce(mockResponseFail); + + const { result } = renderHook(() => useEmailNotification()); + + await result.current.notifyDocumentUpload('12345', { + ccbcNumber: 'CCBC-10001', + documentType: 'Claim & Progress Report', + documentNames: ['sow.xls'], + }); + + expect(Sentry.captureException).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'Error sending email to notify Claim & Progress Report upload', + message: expect.anything(), + }) + ); + }); +}); diff --git a/app/tests/components/Analyst/Project/Claims/ClaimsForm.test.tsx b/app/tests/components/Analyst/Project/Claims/ClaimsForm.test.tsx index da50a757eb..1e71b4dc1a 100644 --- a/app/tests/components/Analyst/Project/Claims/ClaimsForm.test.tsx +++ b/app/tests/components/Analyst/Project/Claims/ClaimsForm.test.tsx @@ -99,7 +99,7 @@ describe('The Claims form', () => { expect(screen.getByText('Add claim')).toBeInTheDocument(); }); - it('Uploads a Claim', async () => { + it('Uploads a Claim and sends notification', async () => { componentTestingHelper.loadQuery(); componentTestingHelper.renderComponent(); @@ -195,6 +195,12 @@ describe('The Claims form', () => { }, }); }); + + expect(fetch).toHaveBeenCalledWith('/api/email/notifyDocumentUpload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: expect.anything(), + }); }); it('can edit a saved Claim', async () => { diff --git a/app/tests/components/Analyst/Project/CommunityProgressReport/CommunityProgressReportForm.test.tsx b/app/tests/components/Analyst/Project/CommunityProgressReport/CommunityProgressReportForm.test.tsx index 07e4af82ce..3238f5f5a9 100644 --- a/app/tests/components/Analyst/Project/CommunityProgressReport/CommunityProgressReportForm.test.tsx +++ b/app/tests/components/Analyst/Project/CommunityProgressReport/CommunityProgressReportForm.test.tsx @@ -84,7 +84,7 @@ describe('The Community Progress Report form', () => { ).toBeInTheDocument(); }); - it('Uploads a Community Progress Report', async () => { + it('Uploads a Community Progress Report and sends a notification', async () => { componentTestingHelper.loadQuery(); componentTestingHelper.renderComponent(); @@ -193,6 +193,12 @@ describe('The Community Progress Report form', () => { }, }); }); + + expect(fetch).toHaveBeenCalledWith('/api/email/notifyDocumentUpload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: expect.anything(), + }); }); it('displays the saved Community Progress Report', () => { diff --git a/app/tests/components/Analyst/Project/Milestones/MilestonesForm.test.tsx b/app/tests/components/Analyst/Project/Milestones/MilestonesForm.test.tsx index f0765f09e8..a4bdc8590b 100644 --- a/app/tests/components/Analyst/Project/Milestones/MilestonesForm.test.tsx +++ b/app/tests/components/Analyst/Project/Milestones/MilestonesForm.test.tsx @@ -236,6 +236,12 @@ describe('The Milestone form', () => { expect(screen.getByText('Edit')).toBeInTheDocument(); expect(screen.getByText('Delete')).toBeInTheDocument(); expect(screen.getByText('Milestone Report')).toBeInTheDocument(); + + expect(fetch).toHaveBeenCalledWith('/api/email/notifyDocumentUpload', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: expect.anything(), + }); }); it('can edit a saved Milestone', async () => { diff --git a/app/tests/components/Analyst/RFI/RFIAnalystUpload.test.ts b/app/tests/components/Analyst/RFI/RFIAnalystUpload.test.ts index 86c43ff9a2..dd7114dcb6 100644 --- a/app/tests/components/Analyst/RFI/RFIAnalystUpload.test.ts +++ b/app/tests/components/Analyst/RFI/RFIAnalystUpload.test.ts @@ -5,16 +5,18 @@ import RFIAnalystUpload from 'components/Analyst/RFI/RFIAnalystUpload'; import compiledQuery, { RFIAnalystUploadTestQuery, } from '__generated__/RFIAnalystUploadTestQuery.graphql'; -import useHHCountUpdateEmail from 'lib/helpers/useHHCountUpdateEmail'; +import useEmailNotification from 'lib/helpers/useEmailNotification'; import useRfiCoverageMapKmzUploadedEmail from 'lib/helpers/useRfiCoverageMapKmzUploadedEmail'; import { mocked } from 'jest-mock'; -jest.mock('lib/helpers/useHHCountUpdateEmail'); +jest.mock('lib/helpers/useEmailNotification'); jest.mock('lib/helpers/useRfiCoverageMapKmzUploadedEmail'); const mockNotifyHHCountUpdate = jest.fn(); -mocked(useHHCountUpdateEmail).mockReturnValue({ +const mockNotifyDocumentUpload = jest.fn(); +mocked(useEmailNotification).mockReturnValue({ notifyHHCountUpdate: mockNotifyHHCountUpdate, + notifyDocumentUpload: mockNotifyDocumentUpload, }); const mockNotifyRfiCoverageMapKmzUploaded = jest.fn(); @@ -336,7 +338,27 @@ describe('The RFIAnalystUpload component', () => { } ); - expect(mockNotifyRfiCoverageMapKmzUploaded).toHaveBeenCalledTimes(1); + expect(mockNotifyHHCountUpdate).toHaveBeenCalledWith( + { + householdsImpactedIndigenous: 123987, + numberOfHouseholds: 2, + }, + { householdsImpactedIndigenous: 13, numberOfHouseholds: 12 }, + 1, + { + ccbcNumber: 'CCBC-12345', + manualUpdate: false, + rfiNumber: 'RFI-01', + timestamp: expect.any(String), + } + ); + + expect(mockNotifyDocumentUpload).toHaveBeenCalledWith(1, { + ccbcNumber: 'CCBC-12345', + documentType: 'Template 1', + documentNames: ['template_one.xlsx'], + }); + expect( screen.getByText(/Template 1 data changed successfully/) ).toBeVisible(); diff --git a/app/tests/pages/analyst/application/[applicationId]/edit/[section].test.tsx b/app/tests/pages/analyst/application/[applicationId]/edit/[section].test.tsx index 0bdf6a637b..69720d0d2c 100644 --- a/app/tests/pages/analyst/application/[applicationId]/edit/[section].test.tsx +++ b/app/tests/pages/analyst/application/[applicationId]/edit/[section].test.tsx @@ -7,13 +7,14 @@ import compiledSectionQuery, { } from '__generated__/SectionQuery.graphql'; import PageTestingHelper from 'tests/utils/pageTestingHelper'; import { mocked } from 'jest-mock'; -import useHHCountUpdateEmail from 'lib/helpers/useHHCountUpdateEmail'; +import useEmailNotification from 'lib/helpers/useEmailNotification'; -jest.mock('lib/helpers/useHHCountUpdateEmail'); +jest.mock('lib/helpers/useEmailNotification'); const mockNotifyHHCountUpdate = jest.fn(); -mocked(useHHCountUpdateEmail).mockReturnValue({ +mocked(useEmailNotification).mockReturnValue({ notifyHHCountUpdate: mockNotifyHHCountUpdate, + notifyDocumentUpload: jest.fn(), }); const mockQueryPayload = { diff --git a/app/utils/formatArray.ts b/app/utils/formatArray.ts new file mode 100644 index 0000000000..ea4072da08 --- /dev/null +++ b/app/utils/formatArray.ts @@ -0,0 +1,8 @@ +const joinWithAnd = (array: string[]): string => { + if (array.length === 0) return ''; + if (array.length === 1) return array[0]; + const lastField = array.pop(); + return `${array.join(', ')} and ${lastField}`; +}; + +export default joinWithAnd; diff --git a/app/utils/formatString.ts b/app/utils/formatString.ts index dd47e645a9..87a00214f3 100644 --- a/app/utils/formatString.ts +++ b/app/utils/formatString.ts @@ -1,8 +1,9 @@ -const transformToTitleCase = (input: string): string => { +const toTitleCase = (input: string, delimiter = ' ') => { return input - .split('_') + .toLowerCase() + .split(delimiter) .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); }; -export default transformToTitleCase; +export default toTitleCase;