diff --git a/app/backend/lib/excel_import/template_nine.ts b/app/backend/lib/excel_import/template_nine.ts index ff414db343..2974cbd504 100644 --- a/app/backend/lib/excel_import/template_nine.ts +++ b/app/backend/lib/excel_import/template_nine.ts @@ -509,6 +509,60 @@ templateNine.get( } ); +templateNine.post( + '/api/template-nine/rfi/applicant/:id/:rfiNumber', + limiter, + async (req, res) => { + const authRole = getAuthRole(req); + const pgRole = authRole?.pgRole; + const isRoleAuthorized = pgRole === 'ccbc_auth_user'; + if (!isRoleAuthorized) { + return res.status(404).end(); + } + + const { id, rfiNumber } = req.params; + + const applicationId = parseInt(id, 10); + + if (!id || !rfiNumber || Number.isNaN(applicationId)) { + return res.status(400).json({ error: 'Invalid parameters' }); + } + const errorList = []; + const form = formidable(commonFormidableConfig); + + let files; + try { + files = await parseForm(form, req); + } catch (err) { + errorList.push({ level: 'file', error: err }); + return res.status(400).json(errorList).end(); + } + const filename = Object.keys(files)[0]; + const uploadedFilesArray = files[filename] as Array; + const uploaded = uploadedFilesArray?.[0]; + + if (!uploaded) { + return res.status(400).end(); + } + const buf = fs.readFileSync(uploaded.filepath); + const wb = XLSX.read(buf); + let templateNineData; + try { + templateNineData = await loadTemplateNineData(wb); + } catch (err) { + errorList.push({ level: 'file', error: err }); + return res.status(400).json(errorList).end(); + } + + if (templateNineData) { + return res.status(200).json(templateNineData); + } + return res + .status(400) + .json({ error: 'Unknown error while parsing template nine' }); + } +); + templateNine.post( '/api/template-nine/rfi/:id/:rfiNumber', limiter, diff --git a/app/lib/theme/widgets/FileWidget.tsx b/app/lib/theme/widgets/FileWidget.tsx index dd4e74d945..67acab8d6f 100644 --- a/app/lib/theme/widgets/FileWidget.tsx +++ b/app/lib/theme/widgets/FileWidget.tsx @@ -27,6 +27,101 @@ interface FileWidgetProps extends WidgetProps { value: Array; } +export async function processFileTemplate( + file: any, + setTemplateData: Function, + templateNumber: number, + isApplicantPage: boolean, + formId: number, + rfiNumber: string +) { + let isTemplateValid = true; + const fileFormData = new FormData(); + if (file) { + fileFormData.append('file', file); + if (setTemplateData) { + try { + if (templateNumber !== 9) { + const response = await fetch( + `/api/applicant/template?templateNumber=${templateNumber}`, + { + method: 'POST', + body: fileFormData, + } + ); + if (response.ok) { + const data = await response.json(); + setTemplateData({ + templateNumber, + data, + templateName: file.name, + }); + } else { + isTemplateValid = false; + setTemplateData({ + templateNumber, + error: true, + }); + } + } else if (templateNumber === 9) { + if (isApplicantPage) { + // fetch for applicant and handle as expected + const response = await fetch( + `/api/template-nine/rfi/applicant/${formId}/${rfiNumber}`, + { + method: 'POST', + body: fileFormData, + } + ); + if (response.ok) { + const data = await response.json(); + setTemplateData({ + templateNumber, + data, + templateName: file.name, + }); + } else { + isTemplateValid = false; + setTemplateData({ + templateNumber, + error: true, + }); + } + } else { + const response = await fetch( + `/api/template-nine/rfi/${formId}/${rfiNumber}`, + { + method: 'POST', + body: fileFormData, + } + ); + if (response.ok) { + await response.json(); + setTemplateData({ + templateNumber, + templateName: file.name, + }); + } else { + isTemplateValid = false; + setTemplateData({ + templateNumber, + error: true, + }); + } + } + } + } catch (error) { + isTemplateValid = false; + setTemplateData({ + templateNumber, + error: true, + }); + } + } + } + return isTemplateValid; +} + const FileWidget: React.FC = ({ id, disabled, @@ -72,6 +167,8 @@ const FileWidget: React.FC = ({ const { setTemplateData, rfiNumber } = formContext; const { showToast, hideToast } = useToast(); + const isApplicantPage = router.pathname.includes('applicant'); + useEffect(() => { if (rawErrors?.length > 0) { setErrors([{ error: 'rjsf_validation' }]); @@ -79,67 +176,16 @@ const FileWidget: React.FC = ({ }, [rawErrors, setErrors]); const getValidatedFile = async (file: any, formId: number) => { - let isTemplateValid = true; - if (templateValidate) { - const fileFormData = new FormData(); - if (file) { - fileFormData.append('file', file); - if (setTemplateData) { - try { - if (templateNumber !== 9) { - const response = await fetch( - `/api/applicant/template?templateNumber=${templateNumber}`, - { - method: 'POST', - body: fileFormData, - } - ); - if (response.ok) { - const data = await response.json(); - setTemplateData({ - templateNumber, - data, - templateName: file.name, - }); - } else { - isTemplateValid = false; - setTemplateData({ - templateNumber, - error: true, - }); - } - } else if (templateNumber === 9) { - const response = await fetch( - `/api/template-nine/rfi/${formId}/${rfiNumber}`, - { - method: 'POST', - body: fileFormData, - } - ); - if (response.ok) { - await response.json(); - setTemplateData({ - templateNumber, - templateName: file.name, - }); - } else { - isTemplateValid = false; - setTemplateData({ - templateNumber, - error: true, - }); - } - } - } catch (error) { - isTemplateValid = false; - setTemplateData({ - templateNumber, - error: true, - }); - } - } - } - } + const isTemplateValid = templateValidate + ? await processFileTemplate( + file, + setTemplateData, + templateNumber, + isApplicantPage, + formId, + rfiNumber + ) + : true; const { name, size, type } = file; const { isValid, error: newError } = validateFile( diff --git a/app/pages/applicantportal/form/[id]/rfi/[applicantRfiId].tsx b/app/pages/applicantportal/form/[id]/rfi/[applicantRfiId].tsx index 0b95f02931..73eaa90409 100644 --- a/app/pages/applicantportal/form/[id]/rfi/[applicantRfiId].tsx +++ b/app/pages/applicantportal/form/[id]/rfi/[applicantRfiId].tsx @@ -15,9 +15,11 @@ import { useRouter } from 'next/router'; import FormDiv from 'components/FormDiv'; import styled from 'styled-components'; import { useEffect, useState } from 'react'; -import { useCreateNewFormDataMutation } from 'schema/mutations/application/createNewFormData'; import useEmailNotification from 'lib/helpers/useEmailNotification'; import useRfiCoverageMapKmzUploadedEmail from 'lib/helpers/useRfiCoverageMapKmzUploadedEmail'; +import { useUpdateRfiAndCreateTemplateNineDataMutation } from 'schema/mutations/application/updateRfiAndCreateTemplateNineDataMutation'; +import { useUpdateRfiAndFormDataMutation } from 'schema/mutations/application/updateRfiAndFormDataMutation'; +import { useUpdateFormRfiAndCreateTemplateNineDataMutation } from 'schema/mutations/application/updateFormRfiAndCreateTemplateNineDataMutation'; const Flex = styled('header')` display: flex; @@ -42,6 +44,13 @@ const getApplicantRfiIdQuery = graphql` id ccbcNumber organizationName + applicationFormTemplate9DataByApplicationId( + filter: { archivedAt: { isNull: true } } + ) { + nodes { + rowId + } + } formData { id formSchemaId @@ -64,20 +73,52 @@ const ApplicantRfiPage = ({ const { session, rfiDataByRowId, applicationByRowId } = query; const { rfiNumber } = rfiDataByRowId; const [updateRfi] = useUpdateWithTrackingRfiMutation(); + const [updateRfiAndCreateTemplateNineData] = + useUpdateRfiAndCreateTemplateNineDataMutation(); + const [updateFormRfiAndCreateTemplateNineData] = + useUpdateFormRfiAndCreateTemplateNineDataMutation(); + const [updateRfiAndFormData] = useUpdateRfiAndFormDataMutation(); const router = useRouter(); const formJsonData = applicationByRowId?.formData?.jsonData; const applicationId = router.query.id as string; const formSchemaId = applicationByRowId?.formData?.formSchemaId; const ccbcNumber = applicationByRowId?.ccbcNumber; + const applicationFormTemplate9DataId = + applicationByRowId?.applicationFormTemplate9DataByApplicationId?.nodes?.[0] + ?.rowId; const [newFormData, setNewFormData] = useState(formJsonData); - const [createNewFormData] = useCreateNewFormDataMutation(); + const [hasApplicationFormDataUpdated, setHasApplicationFormDataUpdated] = + useState(false); + const [templateNineData, setTemplateNineData] = useState(null); const [templateData, setTemplateData] = useState(null); + const [templatesUpdated, setTemplatesUpdated] = useState({ + one: false, + two: false, + nine: false, + }); const [formData, setFormData] = useState(rfiDataByRowId.jsonData); const { notifyHHCountUpdate } = useEmailNotification(); const { notifyRfiCoverageMapKmzUploaded } = useRfiCoverageMapKmzUploadedEmail(); useEffect(() => { + const getFileDetails = (templateNumber) => { + if (templateNumber === 1) { + return formData?.rfiAdditionalFiles + ?.eligibilityAndImpactsCalculator?.[0]; + } + if (templateNumber === 2) { + return formData?.rfiAdditionalFiles?.detailedBudget?.[0]; + } + if (templateNumber === 9) { + return formData?.rfiAdditionalFiles?.geographicNames?.[0]; + } + + return null; + }; + + const fileDetails = getFileDetails(templateData?.templateNumber); + if (templateData?.templateNumber === 1 && !templateData.error) { const newFormDataWithTemplateOne = { ...newFormData, @@ -89,6 +130,11 @@ const ApplicantRfiPage = ({ }, }; setNewFormData(newFormDataWithTemplateOne); + setTemplatesUpdated((prevTemplatesUpdated) => { + return { ...prevTemplatesUpdated, one: true }; + }); + setTemplateData(null); + setHasApplicationFormDataUpdated(true); } else if (templateData?.templateNumber === 2 && !templateData.error) { const newFormDataWithTemplateTwo = { ...newFormData, @@ -99,29 +145,24 @@ const ApplicantRfiPage = ({ }, }; setNewFormData(newFormDataWithTemplateTwo); - } else if (templateData?.error && templateData?.templateNumber === 1) { - const fileArrayLength = - newFormData.templateUploads?.eligibilityAndImpactsCalculator?.length; - fetch(`/api/email/notifyFailedReadOfTemplateData`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - applicationId, - host: window.location.origin, - params: { - templateNumber: templateData.templateNumber, - uuid: newFormData.templateUploads - ?.eligibilityAndImpactsCalculator?.[fileArrayLength - 1]?.uuid, - uploadedAt: - newFormData.templateUploads?.eligibilityAndImpactsCalculator?.[ - fileArrayLength - 1 - ]?.uploadedAt, - }, - }), + setTemplatesUpdated((prevTemplatesUpdated) => { + return { ...prevTemplatesUpdated, two: true }; }); - } else if (templateData?.error && templateData?.templateNumber === 2) { - const fileArrayLength = - newFormData.templateUploads?.detailedBudget?.length; + setHasApplicationFormDataUpdated(true); + setTemplateData(null); + } else if (templateData?.templateNumber === 9 && !templateData.error) { + setTemplatesUpdated((prevTemplatesUpdated) => { + return { ...prevTemplatesUpdated, nine: true }; + }); + setTemplateNineData({ ...templateData }); + setTemplateData(null); + } else if ( + fileDetails && + templateData?.error && + (templateData?.templateNumber === 1 || + templateData?.templateNumber === 2 || + templateData?.templateNumber === 9) + ) { fetch(`/api/email/notifyFailedReadOfTemplateData`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -130,78 +171,180 @@ const ApplicantRfiPage = ({ host: window.location.origin, params: { templateNumber: templateData.templateNumber, - uuid: newFormData.templateUploads?.detailedBudget?.[ - fileArrayLength - 1 - ]?.uuid, - uploadedAt: - newFormData.templateUploads?.detailedBudget?.[fileArrayLength - 1] - ?.uploadedAt, + uuid: fileDetails.uuid, + uploadedAt: fileDetails?.uploadedAt, }, }), + }).then(() => { + setTemplateData(null); }); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [templateData]); + }, [templateData, formData]); const handleSubmit = (e: IChangeEvent) => { - updateRfi({ - variables: { - input: { - jsonData: e.formData, - rfiRowId: rfiDataByRowId.rowId, - }, - }, - onCompleted: () => { - if (e.formData?.rfiAdditionalFiles?.geographicCoverageMap?.length > 0) { - notifyRfiCoverageMapKmzUploaded( - rfiDataByRowId, - e.formData, - applicationId, + const getTemplateNineUUID = () => { + // can be wrong source if there are multiple uploads + return e.formData?.rfiAdditionalFiles?.geographicNames?.[0]?.uuid; + }; + + const checkAndNotifyRfiCoverage = async () => { + if (e.formData?.rfiAdditionalFiles?.geographicCoverageMap?.length > 0) { + return notifyRfiCoverageMapKmzUploaded( + rfiDataByRowId, + e.formData, + applicationId, + ccbcNumber, + rfiNumber, + applicationByRowId.organizationName + ); + } + return Promise.resolve(); + }; + + const checkAndNotifyHHCount = async () => { + if (templatesUpdated?.one) { + return notifyHHCountUpdate( + newFormData.benefits, + formJsonData.benefits, + applicationId, + { ccbcNumber, + timestamp: new Date().toLocaleString(), + manualUpdate: false, rfiNumber, - applicationByRowId.organizationName - ); - } - if (!templateData) { - router.push(`/applicantportal/dashboard`); - } - }, - onError: (err) => { - // eslint-disable-next-line no-console - console.log('Error updating RFI', err); - }, - }); - if (templateData) { - createNewFormData({ + organizationName: applicationByRowId.organizationName, + } + ); + } + return Promise.resolve(); + }; + + if (!hasApplicationFormDataUpdated && !templatesUpdated.nine) { + // form data not updated and template nine not updated + // only update rfi + updateRfi({ variables: { input: { + jsonData: e.formData, + rfiRowId: rfiDataByRowId.rowId, + }, + }, + onCompleted: () => { + setTemplateData(null); + checkAndNotifyRfiCoverage().then(() => { + // wait until email is sent before redirecting + router.push(`/applicantportal/dashboard`); + }); + }, + onError: (err) => { + // eslint-disable-next-line no-console + console.log('Error updating RFI', err); + }, + }); + } else if (!hasApplicationFormDataUpdated && templatesUpdated.nine) { + // form data not updated but template nine updated, update rfi and create template nine record + updateRfiAndCreateTemplateNineData({ + variables: { + rfiInput: { + jsonData: e.formData, + rfiRowId: rfiDataByRowId.rowId, + }, + templateNineInput: { + _applicationId: Number(applicationId), + _jsonData: templateNineData.data, + _previousTemplate9Id: applicationFormTemplate9DataId, + _source: { + source: 'RFI', + uuid: getTemplateNineUUID(), + }, + _errors: templateNineData.data?.errors, + }, + }, + onError: (err) => { + // eslint-disable-next-line no-console + console.log( + 'Error updating RFI and creating template nine data', + err + ); + }, + onCompleted: () => { + setTemplateData(null); + checkAndNotifyRfiCoverage().then(() => { + // wait until email(s) is sent before redirecting + router.push(`/applicantportal/dashboard`); + }); + }, + }); + } else if (hasApplicationFormDataUpdated && !templatesUpdated.nine) { + // only update rfi and form data since no template nine data + updateRfiAndFormData({ + variables: { + formInput: { applicationRowId: Number(applicationId), jsonData: newFormData, reasonForChange: `Auto updated from upload for RFI: ${rfiNumber}`, formSchemaId, }, + rfiInput: { + jsonData: e.formData, + rfiRowId: rfiDataByRowId.rowId, + }, }, onError: (err) => { // eslint-disable-next-line no-console console.log('Error creating new form data', err); }, onCompleted: () => { - if (templateData?.templateNumber === 1) { - notifyHHCountUpdate( - newFormData.benefits, - formJsonData.benefits, - applicationId, - { - ccbcNumber, - timestamp: new Date().toLocaleString(), - manualUpdate: false, - rfiNumber, - organizationName: applicationByRowId.organizationName, - } - ); - } setTemplateData(null); - router.push(`/applicantportal/dashboard`); + checkAndNotifyRfiCoverage().then(() => { + checkAndNotifyHHCount().then(() => { + // wait until email is sent before redirecting + router.push(`/applicantportal/dashboard`); + }); + }); + }, + }); + } else if (hasApplicationFormDataUpdated && templatesUpdated.nine) { + // update rfi, form data, and template nine data (all three) + updateFormRfiAndCreateTemplateNineData({ + variables: { + formInput: { + applicationRowId: Number(applicationId), + jsonData: newFormData, + reasonForChange: `Auto updated from upload for RFI: ${rfiNumber}`, + formSchemaId, + }, + rfiInput: { + jsonData: e.formData, + rfiRowId: rfiDataByRowId.rowId, + }, + templateNineInput: { + _applicationId: Number(applicationId), + _jsonData: templateNineData.data, + _source: { + source: 'RFI', + uuid: getTemplateNineUUID(), + }, + _previousTemplate9Id: applicationFormTemplate9DataId, + _errors: templateNineData.data?.errors, + }, + }, + onError: (err) => { + // eslint-disable-next-line no-console + console.log( + 'Error updating RFI, form data, and template nine data', + err + ); + }, + onCompleted: () => { + setTemplateData(null); + checkAndNotifyHHCount().then(() => { + checkAndNotifyRfiCoverage().then(() => { + // wait until email(s) is sent before redirecting + router.push(`/applicantportal/dashboard`); + }); + }); }, }); } @@ -236,7 +379,11 @@ const ApplicantRfiPage = ({ onChange={handleChange} onSubmit={handleSubmit} noValidate - formContext={{ setTemplateData, skipUnsavedWarning: true }} + formContext={{ + setTemplateData, + skipUnsavedWarning: true, + rfiNumber, + }} > diff --git a/app/schema/mutations/application/updateFormRfiAndCreateTemplateNineDataMutation.ts b/app/schema/mutations/application/updateFormRfiAndCreateTemplateNineDataMutation.ts new file mode 100644 index 0000000000..cc091edc26 --- /dev/null +++ b/app/schema/mutations/application/updateFormRfiAndCreateTemplateNineDataMutation.ts @@ -0,0 +1,38 @@ +import { graphql } from 'react-relay'; +import { updateFormRfiAndCreateTemplateNineDataMutation } from '__generated__/updateFormRfiAndCreateTemplateNineDataMutation.graphql'; +import useMutationWithErrorMessage from '../useMutationWithErrorMessage'; + +const mutation = graphql` + mutation updateFormRfiAndCreateTemplateNineDataMutation( + $rfiInput: UpdateRfiInput! + $templateNineInput: CreateOrUpdateApplicationFormTemplate9DataInput! + $formInput: CreateNewFormDataInput! + ) { + updateRfi(input: $rfiInput) { + rfiData { + rfiNumber + rowId + id + } + } + createOrUpdateApplicationFormTemplate9Data(input: $templateNineInput) { + applicationFormTemplate9Data { + rowId + applicationId + } + } + createNewFormData(input: $formInput) { + formData { + jsonData + } + } + } +`; + +const useUpdateFormRfiAndCreateTemplateNineDataMutation = () => + useMutationWithErrorMessage( + mutation, + () => 'An error occurred while attempting to create the application.' + ); + +export { mutation, useUpdateFormRfiAndCreateTemplateNineDataMutation }; diff --git a/app/schema/mutations/application/updateRfiAndCreateTemplateNineDataMutation.ts b/app/schema/mutations/application/updateRfiAndCreateTemplateNineDataMutation.ts new file mode 100644 index 0000000000..4e1922ff40 --- /dev/null +++ b/app/schema/mutations/application/updateRfiAndCreateTemplateNineDataMutation.ts @@ -0,0 +1,33 @@ +import { graphql } from 'react-relay'; +import { updateRfiAndCreateTemplateNineDataMutation } from '__generated__/updateRfiAndCreateTemplateNineDataMutation.graphql'; +import useMutationWithErrorMessage from '../useMutationWithErrorMessage'; + +const mutation = graphql` + mutation updateRfiAndCreateTemplateNineDataMutation( + $rfiInput: UpdateRfiInput! + $templateNineInput: CreateOrUpdateApplicationFormTemplate9DataInput! + ) { + updateRfi(input: $rfiInput) { + rfiData { + rfiNumber + rowId + id + } + } + createOrUpdateApplicationFormTemplate9Data(input: $templateNineInput) { + applicationFormTemplate9Data { + rowId + applicationId + } + } + } +`; + +const useUpdateRfiAndCreateTemplateNineDataMutation = () => + useMutationWithErrorMessage( + mutation, + () => + 'An error occurred while attempting to update the RFI with new Template Nine Data.' + ); + +export { mutation, useUpdateRfiAndCreateTemplateNineDataMutation }; diff --git a/app/schema/mutations/application/updateRfiAndFormDataMutation.ts b/app/schema/mutations/application/updateRfiAndFormDataMutation.ts new file mode 100644 index 0000000000..9c001468f8 --- /dev/null +++ b/app/schema/mutations/application/updateRfiAndFormDataMutation.ts @@ -0,0 +1,31 @@ +import { graphql } from 'react-relay'; +import { updateRfiAndFormDataMutation } from '__generated__/updateRfiAndFormDataMutation.graphql'; +import useMutationWithErrorMessage from '../useMutationWithErrorMessage'; + +const mutation = graphql` + mutation updateRfiAndFormDataMutation( + $rfiInput: UpdateRfiInput! + $formInput: CreateNewFormDataInput! + ) { + updateRfi(input: $rfiInput) { + rfiData { + rfiNumber + rowId + id + } + } + createNewFormData(input: $formInput) { + formData { + jsonData + } + } + } +`; + +const useUpdateRfiAndFormDataMutation = () => + useMutationWithErrorMessage( + mutation, + () => 'An error occurred while attempting to create the application.' + ); + +export { mutation, useUpdateRfiAndFormDataMutation }; diff --git a/app/schema/schema.graphql b/app/schema/schema.graphql index 9c01552ab6..b1fc006508 100644 --- a/app/schema/schema.graphql +++ b/app/schema/schema.graphql @@ -91071,6 +91071,12 @@ type Mutation { """ input: CreateNewFormDataInput! ): CreateNewFormDataPayload + createOrUpdateApplicationFormTemplate9Data( + """ + The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. + """ + input: CreateOrUpdateApplicationFormTemplate9DataInput! + ): CreateOrUpdateApplicationFormTemplate9DataPayload createPackage( """ The exclusive input argument for this mutation. An object type, make sure to see documentation for this object’s fields. @@ -105486,6 +105492,67 @@ input CreateNewFormDataInput { formSchemaId: Int! } +""" +The output of our `createOrUpdateApplicationFormTemplate9Data` mutation. +""" +type CreateOrUpdateApplicationFormTemplate9DataPayload { + """ + The exact same `clientMutationId` that was provided in the mutation input, + unchanged and unused. May be used by a client to track mutations. + """ + clientMutationId: String + applicationFormTemplate9Data: ApplicationFormTemplate9Data + + """ + Our root query field type. Allows us to run any query from our mutation payload. + """ + query: Query + + """ + Reads a single `Application` that is related to this `ApplicationFormTemplate9Data`. + """ + applicationByApplicationId: Application + + """ + Reads a single `CcbcUser` that is related to this `ApplicationFormTemplate9Data`. + """ + ccbcUserByCreatedBy: CcbcUser + + """ + Reads a single `CcbcUser` that is related to this `ApplicationFormTemplate9Data`. + """ + ccbcUserByUpdatedBy: CcbcUser + + """ + Reads a single `CcbcUser` that is related to this `ApplicationFormTemplate9Data`. + """ + ccbcUserByArchivedBy: CcbcUser + + """ + An edge for our `ApplicationFormTemplate9Data`. May be used by Relay 1. + """ + applicationFormTemplate9DataEdge( + """The method to use when ordering `ApplicationFormTemplate9Data`.""" + orderBy: [ApplicationFormTemplate9DataOrderBy!] = [PRIMARY_KEY_ASC] + ): ApplicationFormTemplate9DataEdge +} + +""" +All input for the `createOrUpdateApplicationFormTemplate9Data` mutation. +""" +input CreateOrUpdateApplicationFormTemplate9DataInput { + """ + An arbitrary string value with no semantic meaning. Will be included in the + payload verbatim. May be used to track mutations by the client. + """ + clientMutationId: String + _applicationId: Int! + _jsonData: JSON! + _errors: JSON! + _source: JSON! + _previousTemplate9Id: Int +} + """The output of our `createPackage` mutation.""" type CreatePackagePayload { """ diff --git a/app/tests/backend/lib/excel_import/template_nine.test.ts b/app/tests/backend/lib/excel_import/template_nine.test.ts index 7ce1694ae6..41b50a9750 100644 --- a/app/tests/backend/lib/excel_import/template_nine.test.ts +++ b/app/tests/backend/lib/excel_import/template_nine.test.ts @@ -324,4 +324,70 @@ describe('The Community Progress Report import', () => { expect(response.status).toBe(200); }); + + it('should process the rfi for applicant', async () => { + mocked(getAuthRole).mockImplementation(() => { + return { + pgRole: 'ccbc_auth_user', + landingRoute: '/', + }; + }); + + const response = await request(app) + .post('/api/template-nine/rfi/applicant/1/CCBC-00001-1') + .set('Content-Type', 'application/json') + .set('Connection', 'keep-alive') + .field('data', JSON.stringify({ name: 'form' })) + .attach('template9', `${__dirname}/template9-complete.xlsx`); + + expect(response.status).toBe(200); + }); + + it('should reject if a guest tries to process applicant rfi endpoint', async () => { + mocked(getAuthRole).mockImplementation(() => { + return { + pgRole: 'ccbc_guest', + landingRoute: '/', + }; + }); + + const response = await request(app).post( + '/api/template-nine/rfi/applicant/1/CCBC-00001-1' + ); + + expect(response.status).toBe(404); + }); + + it('should return 400 if an applicant tries to process bad file', async () => { + mocked(getAuthRole).mockImplementation(() => { + return { + pgRole: 'ccbc_auth_user', + landingRoute: '/', + }; + }); + + const response = await request(app) + .post('/api/template-nine/rfi/applicant/1/CCBC-00001-1') + .set('Content-Type', 'application/json') + .set('Connection', 'keep-alive') + .field('data', JSON.stringify({ name: 'form' })) + .attach('template9', null); + + expect(response.status).toBe(400); + }); + + it('should return 400 if an applicant tries to hit endpoint with bad parameters', async () => { + mocked(getAuthRole).mockImplementation(() => { + return { + pgRole: 'ccbc_auth_user', + landingRoute: '/', + }; + }); + + const response = await request(app).post( + '/api/template-nine/rfi/applicant/null/rfi' + ); + + expect(response.status).toBe(400); + }); }); diff --git a/app/tests/components/Form/FileWidget.test.tsx b/app/tests/components/Form/FileWidget.test.tsx index 6c6992ab49..ff01e10cb6 100644 --- a/app/tests/components/Form/FileWidget.test.tsx +++ b/app/tests/components/Form/FileWidget.test.tsx @@ -1,4 +1,5 @@ import ApplicationForm from 'components/Form/ApplicationForm'; +import { processFileTemplate } from 'lib/theme/widgets/FileWidget'; import { graphql } from 'react-relay'; import compiledQuery, { FileWidgetTestQuery, @@ -537,6 +538,184 @@ describe('The FileWidget', () => { ); }); + describe('processFileTemplate function tests', () => { + // Keep a reference to the original fetch. + const originalFetch = global.fetch; + + beforeEach(() => { + global.fetch = jest.fn(); + }); + + afterEach(() => { + jest.resetAllMocks(); + global.fetch = originalFetch; + }); + + const file = { name: 'test.txt' }; + + test('should return true and do nothing if file is not provided', async () => { + const setTemplateData = jest.fn(); + // file: null, setTemplateData, templateNumber: 1 } + const result = await processFileTemplate(null, setTemplateData, 1); + expect(result).toBe(true); + expect(global.fetch).not.toHaveBeenCalled(); + expect(setTemplateData).not.toHaveBeenCalled(); + }); + + test('should process template (non-9) when response is ok', async () => { + const mockData = { key: 'value' }; + global.fetch.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue(mockData), + }); + const setTemplateData = jest.fn(); + const result = await processFileTemplate( + file, + setTemplateData, + 5, + false, + 1, + 'rfi-1' + ); + expect(global.fetch).toHaveBeenCalledWith( + `/api/applicant/template?templateNumber=5`, + expect.objectContaining({ + method: 'POST', + body: expect.any(FormData), + }) + ); + expect(setTemplateData).toHaveBeenCalledWith({ + templateNumber: 5, + data: mockData, + templateName: file.name, + }); + expect(result).toBe(true); + }); + + test('should set error for non-9 template when response is not ok', async () => { + global.fetch.mockResolvedValue({ + ok: false, + json: jest.fn(), + }); + const setTemplateData = jest.fn(); + const result = await processFileTemplate( + file, + setTemplateData, + 3, + false, + 1, + 'rfi-1' + ); + expect(setTemplateData).toHaveBeenCalledWith({ + templateNumber: 3, + error: true, + }); + expect(result).toBe(false); + }); + + test('should catch error for non-9 template when fetch throws', async () => { + global.fetch.mockRejectedValue(new Error('Network error')); + const setTemplateData = jest.fn(); + const result = await processFileTemplate( + file, + setTemplateData, + 2, + false, + 1, + 'rfi-1' + ); + expect(setTemplateData).toHaveBeenCalledWith({ + templateNumber: 2, + error: true, + }); + expect(result).toBe(false); + }); + + test('should process template 9 when response is ok', async () => { + global.fetch.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue({}), + }); + const setTemplateData = jest.fn(); + const result = await processFileTemplate( + file, + setTemplateData, + 9, + false, + 'form123', + 'rfi456' + ); + expect(global.fetch).toHaveBeenCalledWith( + `/api/template-nine/rfi/form123/rfi456`, + expect.objectContaining({ + method: 'POST', + body: expect.any(FormData), + }) + ); + expect(setTemplateData).toHaveBeenCalledWith({ + templateNumber: 9, + templateName: file.name, + }); + expect(result).toBe(true); + }); + + test('should process applicant template 9 when response is ok', async () => { + global.fetch.mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue({}), + }); + const setTemplateData = jest.fn(); + const result = await processFileTemplate( + file, + setTemplateData, + 9, + true, + 'form123', + 'rfi456' + ); + expect(global.fetch).toHaveBeenCalledWith( + `/api/template-nine/rfi/applicant/form123/rfi456`, + expect.objectContaining({ + method: 'POST', + body: expect.any(FormData), + }) + ); + expect(setTemplateData).toHaveBeenCalledWith({ + templateNumber: 9, + templateName: file.name, + data: {}, + }); + expect(result).toBe(true); + }); + + test('should set error for template 9 when response is not ok', async () => { + global.fetch.mockResolvedValue({ + ok: false, + json: jest.fn(), + }); + const setTemplateData = jest.fn(); + const result = await processFileTemplate( + file, + setTemplateData, + 9, + 'form123', + 'rfi456' + ); + expect(setTemplateData).toHaveBeenCalledWith({ + templateNumber: 9, + error: true, + }); + expect(result).toBe(false); + }); + + test('should return true if setTemplateData is not provided', async () => { + const result = await processFileTemplate(file, undefined, 5); + // With no setTemplateData, no fetch is performed and isTemplateValid remains true. + expect(global.fetch).not.toHaveBeenCalled(); + expect(result).toBe(true); + }); + }); + afterEach(() => { jest.clearAllMocks(); }); diff --git a/app/tests/pages/applicantportal/form/[id]/rfi/[applicantRfiId].test.tsx b/app/tests/pages/applicantportal/form/[id]/rfi/[applicantRfiId].test.tsx index 7a358b00c7..625363e01a 100644 --- a/app/tests/pages/applicantportal/form/[id]/rfi/[applicantRfiId].test.tsx +++ b/app/tests/pages/applicantportal/form/[id]/rfi/[applicantRfiId].test.tsx @@ -12,6 +12,7 @@ const mockQueryPayload = { jsonData: { rfiDueBy: '2022-12-22', rfiAdditionalFiles: { + geographicNamesRfi: true, detailedBudgetRfi: true, eligibilityAndImpactsCalculatorRfi: true, geographicCoverageMapRfi: true, @@ -43,6 +44,17 @@ const mockQueryPayload = { rfi: { updatedAt: '2022-12-01', }, + formData: { + jsonData: {}, + formSchemaId: 'test', + }, + applicationFormTemplate9DataByApplicationId: { + nodes: [ + { + rowId: 1, + }, + ], + }, projectName: 'projName', status: 'Received', }, @@ -65,6 +77,7 @@ describe('The applicantRfiId Page', () => { pageTestingHelper.reinit(); pageTestingHelper.setMockRouterValues({ query: { applicantRfiId: '1', id: '1' }, + pathname: '/applicantportal/form/1/rfi/1', }); }); @@ -97,6 +110,7 @@ describe('The applicantRfiId Page', () => { jsonData: { rfiType: [], rfiAdditionalFiles: { + geographicNamesRfi: true, eligibilityAndImpactsCalculatorRfi: true, detailedBudgetRfi: true, geographicCoverageMapRfi: true, @@ -217,6 +231,170 @@ describe('The applicantRfiId Page', () => { fireEvent.click(screen.getByRole('button', { name: 'Save' })); }); + pageTestingHelper.expectMutationToBeCalled('updateRfiAndFormDataMutation', { + rfiInput: { + jsonData: { + rfiType: [], + rfiAdditionalFiles: { + geographicNamesRfi: true, + detailedBudgetRfi: true, + eligibilityAndImpactsCalculatorRfi: true, + geographicCoverageMapRfi: true, + geographicCoverageMap: [ + { + uuid: 1, + name: '1.kmz', + size: 0, + type: '', + uploadedAt: expect.anything(), + }, + { + uuid: 2, + name: '2.kmz', + size: 0, + type: '', + uploadedAt: expect.anything(), + }, + ], + eligibilityAndImpactsCalculator: [ + { + id: 1, + uuid: 'string', + name: 'file.xlsx', + size: 1, + type: 'application/vnd.ms-excel', + uploadedAt: expect.anything(), + }, + ], + detailedBudget: [ + { + id: 2, + uuid: 'string', + name: 'file.xlsx', + size: 1, + type: 'application/vnd.ms-excel', + uploadedAt: expect.anything(), + }, + ], + }, + rfiDueBy: '2022-12-22', + }, + rfiRowId: 1, + }, + formInput: { + applicationRowId: 1, + jsonData: { + benefits: { + householdsImpactedIndigenous: 60, + numberOfHouseholds: 4, + }, + budgetDetails: { + totalEligibleCosts: 92455, + totalProjectCost: 101230, + }, + }, + reasonForChange: 'Auto updated from upload for RFI: CCBC-01001-01', + formSchemaId: 'test', + }, + }); + }); + it('uses template 1 and 2 data to notify if failed template read', async () => { + pageTestingHelper.loadQuery(); + pageTestingHelper.renderPage(); + + const mockSuccessResponseTemplateOne = { + totalNumberHouseholdsImpacted: 60, + finalEligibleHouseholds: 4, + }; + const mockFetchPromiseTemplateOne = Promise.resolve({ + ok: false, + status: 200, + json: () => Promise.resolve({ result: mockSuccessResponseTemplateOne }), + }); + + const mockSuccessResponseTemplateTwo = { + totalEligibleCosts: 92455, + totalProjectCosts: 101230, + }; + const mockFetchPromiseTemplateTwo = Promise.resolve({ + ok: false, + status: 200, + json: () => Promise.resolve({ result: mockSuccessResponseTemplateTwo }), + }); + global.fetch = jest.fn((url) => { + if (url.includes('templateNumber=1')) return mockFetchPromiseTemplateOne; + return mockFetchPromiseTemplateTwo; + }); + + const file = new File([new ArrayBuffer(1)], 'file.xlsx', { + type: 'application/vnd.ms-excel', + }); + + const inputFile = screen.getAllByTestId('file-test')[0]; + await act(async () => { + fireEvent.change(inputFile, { target: { files: [file] } }); + }); + + await act(async () => { + pageTestingHelper.environment.mock.resolveMostRecentOperation({ + data: { + createAttachment: { + attachment: { + rowId: 1, + file: 'string', + }, + }, + }, + }); + }); + const formData = new FormData(); + formData.append('file', file); + + expect(global.fetch).toHaveBeenCalledTimes(2); + expect(global.fetch).toHaveBeenCalledWith( + '/api/applicant/template?templateNumber=1', + { body: formData, method: 'POST' } + ); + + expect(global.fetch).toHaveBeenCalledWith( + '/api/email/notifyFailedReadOfTemplateData', + { + body: expect.stringContaining( + '{"applicationId":"1","host":"http://localhost","params":{"templateNumber":1,"uuid":"string","uploadedAt":' + ), + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + } + ); + + const inputFile2 = screen.getAllByTestId('file-test')[1]; + await act(async () => { + fireEvent.change(inputFile2, { target: { files: [file] } }); + }); + + await act(async () => { + pageTestingHelper.environment.mock.resolveMostRecentOperation({ + data: { + createAttachment: { + attachment: { + rowId: 2, + file: 'string', + }, + }, + }, + }); + }); + + expect(global.fetch).toHaveBeenCalledTimes(4); + expect(global.fetch).toHaveBeenCalledWith( + '/api/applicant/template?templateNumber=2', + { body: formData, method: 'POST' } + ); + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + }); + pageTestingHelper.expectMutationToBeCalled( 'updateWithTrackingRfiMutation', { @@ -224,6 +402,7 @@ describe('The applicantRfiId Page', () => { jsonData: { rfiType: [], rfiAdditionalFiles: { + geographicNamesRfi: true, eligibilityAndImpactsCalculatorRfi: true, detailedBudgetRfi: true, eligibilityAndImpactsCalculator: [ @@ -271,16 +450,146 @@ describe('The applicantRfiId Page', () => { } ); }); - it('uses template 1 and 2 data to notify if failed template read', async () => { + it('uses template 9 data and creates new record', async () => { + // load page test + // mock fetch for template 9 + // upload fake template 9 + // click save + // expect update rfi and create new record mutation to be called + pageTestingHelper.loadQuery(); pageTestingHelper.renderPage(); + const mockSuccessResponseTemplateNine = { + errors: [], + projectZone: 'zone', + geoName: 'geoName', + }; + + const mockFetchPromiseTemplateNine = Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({ ...mockSuccessResponseTemplateNine }), + }); + + global.fetch = jest.fn((url) => { + if (url.includes('template-nine')) return mockFetchPromiseTemplateNine; + return Promise.resolve({ ok: true, status: 200 }); + }); + + const file = new File([new ArrayBuffer(1)], 'file.xlsx', { + type: 'application/vnd.ms-excel', + }); + // grab the third file input since that is the one for template 9 + const inputFile = screen.getAllByTestId('file-test')[2]; + + await act(async () => { + fireEvent.change(inputFile, { target: { files: [file] } }); + }); + + await act(async () => { + pageTestingHelper.environment.mock.resolveMostRecentOperation({ + data: { + createAttachment: { + attachment: { + rowId: 3, + file: 'UUIDstring', + }, + }, + }, + }); + }); + + const formData = new FormData(); + formData.append('file', file); + + expect(global.fetch).toHaveBeenCalledOnce(); + expect(global.fetch).toHaveBeenCalledWith( + '/api/template-nine/rfi/applicant/1/CCBC-01001-01', + { + body: formData, + method: 'POST', + } + ); + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + }); + + pageTestingHelper.expectMutationToBeCalled( + 'updateRfiAndCreateTemplateNineDataMutation', + { + rfiInput: { + jsonData: { + rfiType: [], + rfiAdditionalFiles: { + geographicNamesRfi: true, + detailedBudgetRfi: true, + eligibilityAndImpactsCalculatorRfi: true, + geographicCoverageMapRfi: true, + geographicCoverageMap: [ + { + uuid: 1, + name: '1.kmz', + size: 0, + type: '', + uploadedAt: '2024-05-31T14:05:03.509-07:00', + }, + { + uuid: 2, + name: '2.kmz', + size: 0, + type: '', + uploadedAt: '2024-05-31T14:05:03.509-07:00', + }, + ], + geographicNames: [ + { + id: 3, + uuid: 'UUIDstring', + name: 'file.xlsx', + size: 1, + type: 'application/vnd.ms-excel', + uploadedAt: expect.anything(), + }, + ], + }, + rfiDueBy: '2022-12-22', + }, + rfiRowId: 1, + }, + templateNineInput: { + _applicationId: 1, + _jsonData: { + errors: [], + projectZone: 'zone', + geoName: 'geoName', + }, + _previousTemplate9Id: 1, + _source: { + source: 'RFI', + uuid: 'UUIDstring', + }, + _errors: [], + }, + } + ); + }); + + it('uses template 1, 2, and 9 data to update form, rfi data, and create new record', async () => { + // load page test + // mock fetch for template 1, 2, and 9 + // upload fake template 1, 2, and 9 + // click save + // expect update rfi, form, and create new record mutation to be called + pageTestingHelper.loadQuery(); + pageTestingHelper.renderPage(); const mockSuccessResponseTemplateOne = { totalNumberHouseholdsImpacted: 60, finalEligibleHouseholds: 4, }; const mockFetchPromiseTemplateOne = Promise.resolve({ - ok: false, + ok: true, status: 200, json: () => Promise.resolve({ result: mockSuccessResponseTemplateOne }), }); @@ -290,75 +599,102 @@ describe('The applicantRfiId Page', () => { totalProjectCosts: 101230, }; const mockFetchPromiseTemplateTwo = Promise.resolve({ - ok: false, + ok: true, status: 200, json: () => Promise.resolve({ result: mockSuccessResponseTemplateTwo }), }); + + const mockSuccessResponseTemplateNine = { + errors: [], + projectZone: 'zone', + geoName: 'geoName', + }; + + const mockFetchPromiseTemplateNine = Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({ ...mockSuccessResponseTemplateNine }), + }); + global.fetch = jest.fn((url) => { if (url.includes('templateNumber=1')) return mockFetchPromiseTemplateOne; - return mockFetchPromiseTemplateTwo; + if (url.includes('templateNumber=2')) return mockFetchPromiseTemplateTwo; + return mockFetchPromiseTemplateNine; }); const file = new File([new ArrayBuffer(1)], 'file.xlsx', { type: 'application/vnd.ms-excel', }); - const inputFile = screen.getAllByTestId('file-test')[0]; + const templateOneInputFile = screen.getAllByTestId('file-test')[0]; + const templateTwoInputFile = screen.getAllByTestId('file-test')[1]; + const templateNineInputFile = screen.getAllByTestId('file-test')[2]; + // upload template 1 await act(async () => { - fireEvent.change(inputFile, { target: { files: [file] } }); + fireEvent.change(templateOneInputFile, { target: { files: [file] } }); }); - - act(() => { + // resolve template 1 upload + await act(async () => { pageTestingHelper.environment.mock.resolveMostRecentOperation({ data: { createAttachment: { attachment: { rowId: 1, - file: 'string', + file: 'UUIDTemplateOne', }, }, }, }); }); - const formData = new FormData(); - formData.append('file', file); - - expect(global.fetch).toHaveBeenCalledTimes(2); - expect(global.fetch).toHaveBeenCalledWith( - '/api/applicant/template?templateNumber=1', - { body: formData, method: 'POST' } - ); - expect(global.fetch).toHaveBeenCalledWith( - '/api/email/notifyFailedReadOfTemplateData', - { - body: '{"applicationId":"1","host":"http://localhost","params":{"templateNumber":1}}', - headers: { 'Content-Type': 'application/json' }, - method: 'POST', - } - ); - - const inputFile2 = screen.getAllByTestId('file-test')[1]; + // upload template 2 await act(async () => { - fireEvent.change(inputFile2, { target: { files: [file] } }); + fireEvent.change(templateTwoInputFile, { target: { files: [file] } }); }); - + // resolve template 2 upload await act(async () => { pageTestingHelper.environment.mock.resolveMostRecentOperation({ data: { createAttachment: { attachment: { rowId: 2, - file: 'string', + file: 'UUIDTemplateTwo', + }, + }, + }, + }); + }); + // upload template 9 + await act(async () => { + fireEvent.change(templateNineInputFile, { target: { files: [file] } }); + }); + // resolve template 9 upload + await act(async () => { + pageTestingHelper.environment.mock.resolveMostRecentOperation({ + data: { + createAttachment: { + attachment: { + rowId: 3, + file: 'UUIDTemplateNine', }, }, }, }); }); - expect(global.fetch).toHaveBeenCalledTimes(4); + expect(global.fetch).toHaveBeenCalledTimes(3); + expect(global.fetch).toHaveBeenCalledWith( + '/api/applicant/template?templateNumber=1', + { body: expect.any(FormData), method: 'POST' } + ); + expect(global.fetch).toHaveBeenCalledWith( '/api/applicant/template?templateNumber=2', - { body: formData, method: 'POST' } + { body: expect.any(FormData), method: 'POST' } + ); + + expect(global.fetch).toHaveBeenCalledWith( + '/api/template-nine/rfi/applicant/1/CCBC-01001-01', + { body: expect.any(FormData), method: 'POST' } ); await act(async () => { @@ -366,18 +702,36 @@ describe('The applicantRfiId Page', () => { }); pageTestingHelper.expectMutationToBeCalled( - 'updateWithTrackingRfiMutation', + 'updateFormRfiAndCreateTemplateNineDataMutation', { - input: { + rfiInput: { jsonData: { rfiType: [], rfiAdditionalFiles: { - eligibilityAndImpactsCalculatorRfi: true, + geographicNamesRfi: true, detailedBudgetRfi: true, + eligibilityAndImpactsCalculatorRfi: true, + geographicCoverageMapRfi: true, + geographicCoverageMap: [ + { + uuid: 1, + name: '1.kmz', + size: 0, + type: '', + uploadedAt: '2024-05-31T14:05:03.509-07:00', + }, + { + uuid: 2, + name: '2.kmz', + size: 0, + type: '', + uploadedAt: '2024-05-31T14:05:03.509-07:00', + }, + ], eligibilityAndImpactsCalculator: [ { id: 1, - uuid: 'string', + uuid: 'UUIDTemplateOne', name: 'file.xlsx', size: 1, type: 'application/vnd.ms-excel', @@ -387,28 +741,21 @@ describe('The applicantRfiId Page', () => { detailedBudget: [ { id: 2, - uuid: 'string', + uuid: 'UUIDTemplateTwo', name: 'file.xlsx', size: 1, type: 'application/vnd.ms-excel', uploadedAt: expect.anything(), }, ], - geographicCoverageMapRfi: true, - geographicCoverageMap: [ + geographicNames: [ { - uuid: 1, - name: '1.kmz', - size: 0, - type: '', - uploadedAt: '2024-05-31T14:05:03.509-07:00', - }, - { - uuid: 2, - name: '2.kmz', - size: 0, - type: '', - uploadedAt: '2024-05-31T14:05:03.509-07:00', + id: 3, + uuid: 'UUIDTemplateNine', + name: 'file.xlsx', + size: 1, + type: 'application/vnd.ms-excel', + uploadedAt: expect.anything(), }, ], }, @@ -416,6 +763,31 @@ describe('The applicantRfiId Page', () => { }, rfiRowId: 1, }, + templateNineInput: { + _previousTemplate9Id: 1, + _applicationId: 1, + _jsonData: mockSuccessResponseTemplateNine, + _source: { + source: 'RFI', + uuid: 'UUIDTemplateNine', + }, + _errors: [], + }, + formInput: { + applicationRowId: 1, + jsonData: { + benefits: { + householdsImpactedIndigenous: 60, + numberOfHouseholds: 4, + }, + budgetDetails: { + totalEligibleCosts: 92455, + totalProjectCost: 101230, + }, + }, + reasonForChange: 'Auto updated from upload for RFI: CCBC-01001-01', + formSchemaId: 'test', + }, } ); }); diff --git a/db/deploy/mutations/create_or_update_application_form_template_9_data.sql b/db/deploy/mutations/create_or_update_application_form_template_9_data.sql new file mode 100644 index 0000000000..675accdbe5 --- /dev/null +++ b/db/deploy/mutations/create_or_update_application_form_template_9_data.sql @@ -0,0 +1,41 @@ +-- Deploy ccbc:mutations/create_or_update_application_form_template_9_data to pg + +begin; + +create or replace function ccbc_public.create_or_update_application_form_template_9_data( + _application_id int, + _json_data jsonb, + _errors jsonb, + _source jsonb, + _previous_template_9_id int default null + ) + returns ccbc_public.application_form_template_9_data as + $$ + declare + result ccbc_public.application_form_template_9_data; + begin + -- if previous template 9 id is provided, update the existing record + -- else create a new record + + -- return the updated or created record + + if _previous_template_9_id is not null then + update ccbc_public.application_form_template_9_data + set + application_id = _application_id, + json_data = _json_data, + errors = _errors, + source = _source + where id = _previous_template_9_id + returning * into result; + else + insert into ccbc_public.application_form_template_9_data(application_id, json_data, errors, source) + values (_application_id, _json_data, _errors, _source) + returning * into result; + end if; + + return result; + end; + $$ language plpgsql volatile; + +commit; diff --git a/db/deploy/tables/application_form_template_9_data_002_add_auth_user_role_permission.sql b/db/deploy/tables/application_form_template_9_data_002_add_auth_user_role_permission.sql new file mode 100644 index 0000000000..ba419edbbc --- /dev/null +++ b/db/deploy/tables/application_form_template_9_data_002_add_auth_user_role_permission.sql @@ -0,0 +1,16 @@ +-- Deploy ccbc:tables/application_form_template_9_data_002_add_auth_user_role_permission to pg + +begin; + +do +$grant$ +begin + +perform ccbc_private.grant_permissions('select', 'application_form_template_9_data', 'ccbc_auth_user'); +perform ccbc_private.grant_permissions('insert', 'application_form_template_9_data', 'ccbc_auth_user'); +perform ccbc_private.grant_permissions('update', 'application_form_template_9_data', 'ccbc_auth_user'); + +end +$grant$; + +commit; diff --git a/db/revert/mutations/create_or_update_application_form_template_9_data.sql b/db/revert/mutations/create_or_update_application_form_template_9_data.sql new file mode 100644 index 0000000000..c6dc4ec82e --- /dev/null +++ b/db/revert/mutations/create_or_update_application_form_template_9_data.sql @@ -0,0 +1,7 @@ +-- Revert ccbc:mutations/create_or_update_application_form_template_9_data from pg + +BEGIN; + +drop function ccbc_public.create_or_update_application_form_template_9_data(int, jsonb, jsonb, jsonb, int); + +COMMIT; diff --git a/db/revert/tables/application_form_template_9_data_002_add_auth_user_role_permission.sql b/db/revert/tables/application_form_template_9_data_002_add_auth_user_role_permission.sql new file mode 100644 index 0000000000..f2d74eeb07 --- /dev/null +++ b/db/revert/tables/application_form_template_9_data_002_add_auth_user_role_permission.sql @@ -0,0 +1,9 @@ +-- Revert ccbc:tables/application_form_template_9_data_002_add_auth_user_role_permission from pg + +begin; + +revoke select on ccbc_public.application_form_template_9_data from ccbc_auth_user; +revoke insert on ccbc_public.application_form_template_9_data from ccbc_auth_user; +revoke update on ccbc_public.application_form_template_9_data from ccbc_auth_user; + +commit; diff --git a/db/sqitch.plan b/db/sqitch.plan index 88e42c2d94..2e0173da97 100644 --- a/db/sqitch.plan +++ b/db/sqitch.plan @@ -815,3 +815,5 @@ computed_columns/cbc_history [computed_columns/cbc_history@1.230.0] 2025-02-19T2 tables/application_fnha_contribution 2025-02-24T19:40:28Z ,,, # Add fnha contribution table mutations/save_fnha_contribution 2025-02-25T16:48:17Z ,,, # Add mutation to save fnha contribution @1.246.0 2025-03-05T22:03:48Z CCBC Service Account # release v1.246.0 +tables/application_form_template_9_data_002_add_auth_user_role_permission 2025-02-05T15:11:34Z Anthony Bushara # Adds permissions for an applicant to insert and read template 9 data +mutations/create_or_update_application_form_template_9_data 2025-02-06T16:46:05Z Anthony Bushara # Move the decision making of updating or creating template 9 data from frontend to the backend to create cleaner code