diff --git a/app/client/src/components/app.tsx b/app/client/src/components/app.tsx index 98945305..43639c79 100644 --- a/app/client/src/components/app.tsx +++ b/app/client/src/components/app.tsx @@ -40,6 +40,8 @@ import { FRF2022 } from "@/routes/frf2022"; import { PRF2022 } from "@/routes/prf2022"; import { CRF2022 } from "@/routes/crf2022"; import { FRF2023 } from "@/routes/frf2023"; +import { PRF2023 } from "@/routes/prf2023"; +// import { CRF2023 } from "@/routes/crf2023"; import { useDialogState, useDialogActions } from "@/contexts/dialog"; /** Custom hook to display a site-wide alert banner */ @@ -249,6 +251,8 @@ export function App() { } /> } /> + } /> + {/* } /> */} } /> diff --git a/app/client/src/config.tsx b/app/client/src/config.tsx index d439f5d8..3437fa1f 100644 --- a/app/client/src/config.tsx +++ b/app/client/src/config.tsx @@ -32,7 +32,7 @@ const formioProjectName = VITE_FORMIO_PROJECT_NAME; export const formioProjectUrl = `${formioBaseUrl}/${formioProjectName}`; export const messages = { - genericError: "Something went wrong.", + genericError: "The application has encountered an unknown error.", authError: "Authentication error. Please log in again or contact support.", samlError: "Error logging in. Please try again or contact support.", bapSamFetchError: "Error loading SAM.gov data. Please contact support.", diff --git a/app/client/src/routes/frf2023.tsx b/app/client/src/routes/frf2023.tsx index 9862361f..a7f7c2c6 100644 --- a/app/client/src/routes/frf2023.tsx +++ b/app/client/src/routes/frf2023.tsx @@ -280,8 +280,8 @@ function FundingRequestForm(props: { email: string }) { postData(url, { mongoId: prf._id, - rebateId: prf.data.hidden_bap_rebate_id, - comboKey: prf.data.bap_hidden_entity_combo_key, + rebateId: prf.data._bap_rebate_id, + comboKey: prf.data._bap_entity_combo_key, }) .then((_res) => { window.location.reload(); diff --git a/app/client/src/routes/frfNew.tsx b/app/client/src/routes/frfNew.tsx index 182f8a38..622682ce 100644 --- a/app/client/src/routes/frfNew.tsx +++ b/app/client/src/routes/frfNew.tsx @@ -34,15 +34,22 @@ function createInitialSubmissionData(options: { const { rebateYear, email, entity } = options; const { title, name } = getUserInfo(email, entity); - const comboKey = entity.ENTITY_COMBO_KEY__c; - const uei = entity.UNIQUE_ENTITY_ID__c; - const efti = entity.ENTITY_EFT_INDICATOR__c; - const orgName = entity.LEGAL_BUSINESS_NAME__c; - const address1 = entity.PHYSICAL_ADDRESS_LINE_1__c; - const address2 = entity.PHYSICAL_ADDRESS_LINE_2__c; - const city = entity.PHYSICAL_ADDRESS_CITY__c; - const state = entity.PHYSICAL_ADDRESS_PROVINCE_OR_STATE__c; - const zip = entity.PHYSICAL_ADDRESS_ZIPPOSTAL_CODE__c; + + const { + ENTITY_COMBO_KEY__c, + UNIQUE_ENTITY_ID__c, + ENTITY_EFT_INDICATOR__c, + LEGAL_BUSINESS_NAME__c, + PHYSICAL_ADDRESS_LINE_1__c, + PHYSICAL_ADDRESS_LINE_2__c, + PHYSICAL_ADDRESS_CITY__c, + PHYSICAL_ADDRESS_PROVINCE_OR_STATE__c, + PHYSICAL_ADDRESS_ZIPPOSTAL_CODE__c, + ELEC_BUS_POC_EMAIL__c, + ALT_ELEC_BUS_POC_EMAIL__c, + GOVT_BUS_POC_EMAIL__c, + ALT_GOVT_BUS_POC_EMAIL__c, + } = entity; return rebateYear === "2022" ? { @@ -50,40 +57,40 @@ function createInitialSubmissionData(options: { hidden_current_user_email: email, hidden_current_user_title: title, hidden_current_user_name: name, - bap_hidden_entity_combo_key: comboKey, + bap_hidden_entity_combo_key: ENTITY_COMBO_KEY__c, sam_hidden_applicant_email: email, sam_hidden_applicant_title: title, sam_hidden_applicant_name: name, - sam_hidden_applicant_efti: efti, - sam_hidden_applicant_uei: uei, - sam_hidden_applicant_organization_name: orgName, - sam_hidden_applicant_street_address_1: address1, - sam_hidden_applicant_street_address_2: address2, - sam_hidden_applicant_city: city, - sam_hidden_applicant_state: state, - sam_hidden_applicant_zip_code: zip, + sam_hidden_applicant_efti: ENTITY_EFT_INDICATOR__c, + sam_hidden_applicant_uei: UNIQUE_ENTITY_ID__c, + sam_hidden_applicant_organization_name: LEGAL_BUSINESS_NAME__c, + sam_hidden_applicant_street_address_1: PHYSICAL_ADDRESS_LINE_1__c, + sam_hidden_applicant_street_address_2: PHYSICAL_ADDRESS_LINE_2__c, + sam_hidden_applicant_city: PHYSICAL_ADDRESS_CITY__c, + sam_hidden_applicant_state: PHYSICAL_ADDRESS_PROVINCE_OR_STATE__c, + sam_hidden_applicant_zip_code: PHYSICAL_ADDRESS_ZIPPOSTAL_CODE__c, } : rebateYear === "2023" ? { _user_email: email, _user_title: title, _user_name: name, - _bap_entity_combo_key: comboKey, - _bap_elec_bus_poc_email: entity.ELEC_BUS_POC_EMAIL__c, - _bap_alt_elec_bus_poc_email: entity.ALT_ELEC_BUS_POC_EMAIL__c, - _bap_govt_bus_poc_email: entity.GOVT_BUS_POC_EMAIL__c, - _bap_alt_govt_bus_poc_email: entity.ALT_GOVT_BUS_POC_EMAIL__c, + _bap_entity_combo_key: ENTITY_COMBO_KEY__c, + _bap_elec_bus_poc_email: ELEC_BUS_POC_EMAIL__c, + _bap_alt_elec_bus_poc_email: ALT_ELEC_BUS_POC_EMAIL__c, + _bap_govt_bus_poc_email: GOVT_BUS_POC_EMAIL__c, + _bap_alt_govt_bus_poc_email: ALT_GOVT_BUS_POC_EMAIL__c, _bap_applicant_email: email, _bap_applicant_title: title, _bap_applicant_name: name, - _bap_applicant_efti: efti, - _bap_applicant_uei: uei, - _bap_applicant_organization_name: orgName, - _bap_applicant_street_address_1: address1, - _bap_applicant_street_address_2: address2, - _bap_applicant_city: city, - _bap_applicant_state: state, - _bap_applicant_zip: zip, + _bap_applicant_efti: ENTITY_EFT_INDICATOR__c, + _bap_applicant_uei: UNIQUE_ENTITY_ID__c, + _bap_applicant_organization_name: LEGAL_BUSINESS_NAME__c, + _bap_applicant_street_address_1: PHYSICAL_ADDRESS_LINE_1__c, + _bap_applicant_street_address_2: PHYSICAL_ADDRESS_LINE_2__c, + _bap_applicant_city: PHYSICAL_ADDRESS_CITY__c, + _bap_applicant_state: PHYSICAL_ADDRESS_PROVINCE_OR_STATE__c, + _bap_applicant_zip: PHYSICAL_ADDRESS_ZIPPOSTAL_CODE__c, } : null; } diff --git a/app/client/src/routes/prf2023.tsx b/app/client/src/routes/prf2023.tsx new file mode 100644 index 00000000..6b20889d --- /dev/null +++ b/app/client/src/routes/prf2023.tsx @@ -0,0 +1,461 @@ +import { useEffect, useMemo, useRef } from "react"; +import { useNavigate, useOutletContext, useParams } from "react-router-dom"; +import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query"; +import { Dialog } from "@headlessui/react"; +import { Formio, Form } from "@formio/react"; +import s3 from "formiojs/providers/storage/s3"; +import { cloneDeep, isEqual } from "lodash"; +import icons from "uswds/img/sprite.svg"; +// --- +import { serverUrl, messages } from "@/config"; +import { + getData, + postData, + useContentData, + useConfigData, + useBapSamData, + useSubmissionsQueries, + useSubmissions, + submissionNeedsEdits, + getUserInfo, +} from "@/utilities"; +import { Loading } from "@/components/loading"; +import { Message } from "@/components/message"; +import { MarkdownContent } from "@/components/markdownContent"; +import { useNotificationsActions } from "@/contexts/notifications"; +import { useRebateYearState } from "@/contexts/rebateYear"; +import type { FormioPRF2023Submission } from "@/utilities"; + +type ServerResponse = + | { + userAccess: false; + formSchema: null; + submission: null; + } + | { + userAccess: true; + formSchema: { url: string; json: object }; + submission: FormioPRF2023Submission; + }; + +/** Custom hook to fetch Formio submission data */ +function useFormioSubmissionQueryAndMutation(rebateId: string | undefined) { + const queryClient = useQueryClient(); + + useEffect(() => { + queryClient.resetQueries({ queryKey: ["formio/2023/prf-submission"] }); + }, [queryClient]); + + const url = `${serverUrl}/api/formio/2023/prf-submission/${rebateId}`; + + const query = useQuery({ + queryKey: ["formio/2023/prf-submission", { id: rebateId }], + queryFn: () => { + return getData(url).then((res) => { + const mongoId = res.submission?._id; + const comboKey = res.submission?.data._bap_entity_combo_key; + + /** + * Change the formUrl the File component's `uploadFile` uses, so the s3 + * upload PUT request is routed through the server app. + * + * https://github.com/formio/formio.js/blob/master/src/components/file/File.js#L760 + * https://github.com/formio/formio.js/blob/master/src/providers/storage/s3.js#L5 + * https://github.com/formio/formio.js/blob/master/src/providers/storage/xhr.js#L90 + */ + Formio.Providers.providers.storage.s3 = function (formio: { + formUrl: string; + [field: string]: unknown; + }) { + const s3Formio = cloneDeep(formio); + s3Formio.formUrl = `${serverUrl}/api/formio/2023/s3/prf/${mongoId}/${comboKey}`; + return s3(s3Formio); + }; + + return Promise.resolve(res); + }); + }, + refetchOnWindowFocus: false, + }); + + const mutation = useMutation({ + mutationFn: (updatedSubmission: { + mongoId: string; + submission: { + data: { [field: string]: unknown }; + metadata: { [field: string]: unknown }; + state: "submitted" | "draft"; + }; + }) => { + return postData(url, updatedSubmission); + }, + onSuccess: (res) => { + return queryClient.setQueryData( + ["formio/2023/prf-submission", { id: rebateId }], + (prevData) => { + return prevData?.submission + ? { ...prevData, submission: res } + : prevData; + }, + ); + }, + }); + + return { query, mutation }; +} + +export function PRF2023() { + const { email } = useOutletContext<{ email: string }>(); + /* ensure user verification (JWT refresh) doesn't cause form to re-render */ + return useMemo(() => { + return ; + }, [email]); +} + +function PaymentRequestForm(props: { email: string }) { + const { email } = props; + + const navigate = useNavigate(); + const { id: rebateId } = useParams<"id">(); // CSB Rebate ID (6 digits) + + const content = useContentData(); + const configData = useConfigData(); + const bapSamData = useBapSamData(); + const { + displaySuccessNotification, + displayErrorNotification, + dismissNotification, + } = useNotificationsActions(); + const { rebateYear } = useRebateYearState(); + + const submissionsQueries = useSubmissionsQueries("2023"); + const submissions = useSubmissions("2023"); + + const { query, mutation } = useFormioSubmissionQueryAndMutation(rebateId); + const { userAccess, formSchema, submission } = query.data ?? {}; + + /** + * Stores when data is being posted to the server, so a loading overlay can + * be rendered over the form, preventing the user from losing input data when + * the form is re-rendered with data returned from the server's successful + * post response. + */ + const dataIsPosting = useRef(false); + + /** + * Stores when the form is being submitted, so it can be referenced in the + * Form component's `onSubmit` event prop to prevent double submits. + */ + const formIsBeingSubmitted = useRef(false); + + /** + * Stores the form data's state right after the user clicks the Save, Submit, + * or Next button. As soon as a post request to update the data succeeds, this + * pending submission data is reset to an empty object. This pending data, + * along with the submission data returned from the server is passed into the + * Form component's `submission` prop. + */ + const pendingSubmissionData = useRef<{ [field: string]: unknown }>({}); + + /** + * Stores the last succesfully submitted data, so it can be used in the Form + * component's `onNextPage` event prop's "dirty check" which determines if + * posting of updated data is needed (so we don't make needless requests if no + * field data in the form has changed). + */ + const lastSuccesfullySubmittedData = useRef<{ [field: string]: unknown }>({}); + + if (!configData || !bapSamData) { + return ; + } + + if (submissionsQueries.some((query) => query.isFetching)) { + return ; + } + + if (submissionsQueries.some((query) => query.isError)) { + return ; + } + + if (query.isInitialLoading) { + return ; + } + + if (query.isError || !userAccess || !formSchema || !submission) { + return ; + } + + const rebate = submissions.find((r) => r.rebateId === rebateId); + + const frfNeedsEdits = !rebate + ? false + : submissionNeedsEdits({ + formio: rebate.frf.formio, + bap: rebate.frf.bap, + }); + + const prfNeedsEdits = !rebate + ? false + : submissionNeedsEdits({ + formio: rebate.prf.formio, + bap: rebate.prf.bap, + }); + + const prfSubmissionPeriodOpen = + configData.submissionPeriodOpen[rebateYear].prf; + + const formIsReadOnly = + frfNeedsEdits || + ((submission.state === "submitted" || !prfSubmissionPeriodOpen) && + !prfNeedsEdits); + + /** matched SAM.gov entity for the Payment Request submission */ + const entity = bapSamData.entities.find((entity) => { + const { ENTITY_COMBO_KEY__c } = entity; + return ENTITY_COMBO_KEY__c === submission.data._bap_entity_combo_key; + }); + + if (!entity) { + return ; + } + + if (entity.ENTITY_STATUS__c !== "Active") { + return ; + } + + const { + ELEC_BUS_POC_EMAIL__c, + ALT_ELEC_BUS_POC_EMAIL__c, + GOVT_BUS_POC_EMAIL__c, + ALT_GOVT_BUS_POC_EMAIL__c, + } = entity; + + const { title, name } = getUserInfo(email, entity); + + return ( + + {content && ( + + )} + + {frfNeedsEdits && ( + + )} + + + + + + + + + + Rebate ID: {rebateId} + + + + + {}}> + + + + + + + + + + + + { + if (formIsReadOnly) return; + + // account for when form is being submitted to prevent double submits + if (formIsBeingSubmitted.current) return; + if (onSubmitSubmission.state === "submitted") { + formIsBeingSubmitted.current = true; + } + + const data = { ...onSubmitSubmission.data }; + + const updatedSubmission = { + mongoId: submission._id, + submission: { + ...onSubmitSubmission, + data, + }, + }; + + dismissNotification({ id: 0 }); + dataIsPosting.current = true; + pendingSubmissionData.current = data; + + mutation.mutate(updatedSubmission, { + onSuccess: (res, _payload, _context) => { + pendingSubmissionData.current = {}; + lastSuccesfullySubmittedData.current = cloneDeep(res.data); + + /** success notification id */ + const id = Date.now(); + + displaySuccessNotification({ + id, + body: ( + + {onSubmitSubmission.state === "submitted" ? ( + <> + Payment Request {rebateId} submitted + successfully. + > + ) : ( + <>Draft saved successfully.> + )} + + ), + }); + + if (onSubmitSubmission.state === "submitted") { + /** + * NOTE: we'll keep the success notification displayed and + * redirect the user to their dashboard + */ + navigate("/"); + } + + if (onSubmitSubmission.state === "draft") { + setTimeout(() => dismissNotification({ id }), 5000); + } + }, + onError: (_error, _payload, _context) => { + displayErrorNotification({ + id: Date.now(), + body: ( + + {onSubmitSubmission.state === "submitted" ? ( + <>Error submitting Payment Request form.> + ) : ( + <>Error saving draft.> + )} + + ), + }); + }, + onSettled: (_data, _error, _payload, _context) => { + dataIsPosting.current = false; + formIsBeingSubmitted.current = false; + }, + }); + }} + onNextPage={(onNextPageParam: { + page: number; + submission: { + data: { [field: string]: unknown }; + metadata: { [field: string]: unknown }; + }; + }) => { + if (formIsReadOnly) return; + + const data = { ...onNextPageParam.submission.data }; + + // "dirty check" – don't post an update if no changes have been made + // to the form (ignoring current user fields) + const currentData = { ...data }; + const submittedData = { ...lastSuccesfullySubmittedData.current }; + + delete currentData._user_email; + delete currentData._user_title; + delete currentData._user_name; + delete submittedData._user_email; + delete submittedData._user_title; + delete submittedData._user_name; + if (isEqual(currentData, submittedData)) return; + + const updatedSubmission = { + mongoId: submission._id, + submission: { + ...onNextPageParam.submission, + data, + state: "draft" as const, + }, + }; + + dismissNotification({ id: 0 }); + dataIsPosting.current = true; + pendingSubmissionData.current = data; + + mutation.mutate(updatedSubmission, { + onSuccess: (res, _payload, _context) => { + pendingSubmissionData.current = {}; + lastSuccesfullySubmittedData.current = cloneDeep(res.data); + + /** success notification id */ + const id = Date.now(); + + displaySuccessNotification({ + id, + body: ( + + Draft saved successfully. + + ), + }); + + setTimeout(() => dismissNotification({ id }), 5000); + }, + onError: (_error, _payload, _context) => { + displayErrorNotification({ + id: Date.now(), + body: ( + + Error saving draft. + + ), + }); + }, + onSettled: (_data, _error, _payload, _context) => { + dataIsPosting.current = false; + }, + }); + }} + /> + + + {frfNeedsEdits && ( + + )} + + ); +} diff --git a/app/client/src/routes/submissions.tsx b/app/client/src/routes/submissions.tsx index 6c024466..508a7195 100644 --- a/app/client/src/routes/submissions.tsx +++ b/app/client/src/routes/submissions.tsx @@ -29,7 +29,7 @@ import type { FormioPRF2022Submission, FormioCRF2022Submission, FormioFRF2023Submission, - // FormioPRF2023Submission, + FormioPRF2023Submission, // FormioCRF2023Submission, Rebate, } from "@/utilities"; @@ -37,7 +37,7 @@ import type { const defaultTableRowClassNames = "bg-gray-5"; const highlightedTableRowClassNames = "bg-primary-lighter"; -function ButtonLink(props: { type: "edit" | "view"; to: LinkProps["to"] }) { +function FormButtonLink(props: { type: "edit" | "view"; to: LinkProps["to"] }) { const icon = props.type === "edit" ? "edit" : "visibility"; const text = props.type === "edit" ? "Edit" : "View"; const linkClassNames = @@ -241,11 +241,11 @@ function FRF2022Submission(props: { rebate: Rebate }) { > {frfNeedsEdits ? ( - + ) : frf.formio.state === "submitted" || !frfSubmissionPeriodOpen ? ( - + ) : frf.formio.state === "draft" ? ( - + ) : null} @@ -566,13 +566,13 @@ function PRF2022Submission(props: { rebate: Rebate }) { > {frfNeedsEdits ? ( - + ) : prfNeedsEdits ? ( - + ) : prf.formio.state === "submitted" || !prfSubmissionPeriodOpen ? ( - + ) : prf.formio.state === "draft" ? ( - + ) : null} @@ -790,11 +790,11 @@ function CRF2022Submission(props: { rebate: Rebate }) { > {crfNeedsEdits ? ( - + ) : crf.formio.state === "submitted" || !crfSubmissionPeriodOpen ? ( - + ) : crf.formio.state === "draft" ? ( - + ) : null} @@ -948,11 +948,11 @@ function FRF2023Submission(props: { rebate: Rebate }) { > {frfNeedsEdits ? ( - + ) : frf.formio.state === "submitted" || !frfSubmissionPeriodOpen ? ( - + ) : frf.formio.state === "draft" ? ( - + ) : null} @@ -1048,6 +1048,245 @@ function FRF2023Submission(props: { rebate: Rebate }) { ); } +function PRF2023Submission(props: { rebate: Rebate }) { + const { rebate } = props; + const { frf, prf, crf } = rebate; + + const navigate = useNavigate(); + const { email } = useOutletContext<{ email: string }>(); + + const configData = useConfigData(); + const bapSamData = useBapSamData(); + const { displayErrorNotification } = useNotificationsActions(); + + /** + * Stores when data is being posted to the server, so a loading indicator can + * be rendered inside the "New Payment Request" button, and we can prevent + * double submits/creations of new PRF submissions. + */ + const [dataIsPosting, setDataIsPosting] = useState(false); + + if (!configData || !bapSamData) return null; + + const prfSubmissionPeriodOpen = configData.submissionPeriodOpen["2023"].prf; + + const frfSelected = frf.bap?.status === "Accepted"; + + const frfSelectedButNoPRF = frfSelected && !Boolean(prf.formio); + + /** matched SAM.gov entity for the FRF submission */ + const entity = bapSamData.entities.find((entity) => { + const { ENTITY_STATUS__c, ENTITY_COMBO_KEY__c } = entity; + const comboKey = (frf.formio as FormioFRF2023Submission).data + ._bap_entity_combo_key; + return ENTITY_STATUS__c === "Active" && ENTITY_COMBO_KEY__c === comboKey; + }); + + if (frfSelectedButNoPRF) { + return ( + + + { + if (!prfSubmissionPeriodOpen) return; + if (!frf.bap || !entity) return; + + // account for when data is posting to prevent double submits + if (dataIsPosting) return; + setDataIsPosting(true); + + const { title, name } = getUserInfo(email, entity); + + // create a new draft PRF submission + postData(`${serverUrl}/api/formio/2023/prf-submission/`, { + email, + title, + name, + entity, + comboKey: frf.bap.comboKey, + rebateId: frf.bap.rebateId, // CSB Rebate ID (6 digits) + frfReviewItemId: frf.bap.reviewItemId, // CSB Rebate ID with form/version ID (9 digits) + frfFormModified: frf.bap.modified, + }) + .then((_res) => { + navigate(`/prf/2023/${frf.bap?.rebateId}`); + }) + .catch((_err) => { + displayErrorNotification({ + id: Date.now(), + body: ( + <> + + Error creating Payment Request{" "} + {frf.bap?.rebateId}. + + + Please try again. + + > + ), + }); + }) + .finally(() => { + setDataIsPosting(false); + }); + }} + > + + + + + New Payment Request + {dataIsPosting && } + + + + + ); + } + + // return if a Payment Request submission has not been created for this rebate + if (!prf.formio) return null; + + const { + _user_email, + _bap_rebate_id, // + } = (prf.formio as FormioPRF2023Submission).data; + + const date = new Date(prf.formio.modified).toLocaleDateString(); + const time = new Date(prf.formio.modified).toLocaleTimeString(); + + const frfNeedsEdits = submissionNeedsEdits({ + formio: frf.formio, + bap: frf.bap, + }); + + const prfNeedsEdits = submissionNeedsEdits({ + formio: prf.formio, + bap: prf.bap, + }); + + const prfNeedsClarification = prf.bap?.status === "Needs Clarification"; + + const prfHasBeenWithdrawn = prf.bap?.status === "Withdrawn"; + + const prfFundingNotApproved = prf.bap?.status === "Coordinator Denied"; + + const prfFundingApproved = prf.bap?.status === "Accepted"; + + const prfFundingApprovedButNoCRF = prfFundingApproved && !Boolean(crf.formio); + + const statusTableCellClassNames = + prf.formio.state === "submitted" || !prfSubmissionPeriodOpen + ? "text-italic" + : ""; + + const statusIconClassNames = prfFundingApproved + ? "usa-icon text-primary" // blue + : "usa-icon"; + + const statusIcon = prfNeedsEdits + ? `${icons}#priority_high` // ! + : prfHasBeenWithdrawn + ? `${icons}#close` // ✕ + : prfFundingNotApproved + ? `${icons}#cancel` // ✕ inside a circle + : prfFundingApproved + ? `${icons}#check_circle` // check inside a circle + : prf.formio.state === "draft" + ? `${icons}#more_horiz` // three horizontal dots + : prf.formio.state === "submitted" + ? `${icons}#check` // check + : `${icons}#remove`; // — (fallback, not used) + + const statusText = prfNeedsEdits + ? "Edits Requested" + : prfHasBeenWithdrawn + ? "Withdrawn" + : prfFundingNotApproved + ? "Funding Denied" + : prfFundingApproved + ? "Funding Approved" + : prf.formio.state === "draft" + ? "Draft" + : prf.formio.state === "submitted" + ? "Submitted" + : ""; // fallback, not used + + const prfUrl = `/prf/2023/${_bap_rebate_id}`; + + return ( + + + {frfNeedsEdits ? ( + + ) : prfNeedsEdits ? ( + + ) : prf.formio.state === "submitted" || !prfSubmissionPeriodOpen ? ( + + ) : prf.formio.state === "draft" ? ( + + ) : null} + + + + + + Payment Request + + + {prfNeedsClarification ? ( + + ) : ( + <> + + + + {statusText} + > + )} + + + + + + + + + {_user_email} + + {date} + + + ); +} + +// function CRF2023Submission(props: { rebate: Rebate }) { +// // +// } + function Submissions2022() { const content = useContentData(); const submissionsQueries = useSubmissionsQueries("2022"); @@ -1150,6 +1389,8 @@ function Submissions2023() { return rebate.rebateYear === "2023" ? ( + + {/* */} {/* blank row after all submissions but the last one */} {index !== submissions.length - 1 && ( diff --git a/app/server/.env.example b/app/server/.env.example index 1b80f0ef..334f7a6d 100644 --- a/app/server/.env.example +++ b/app/server/.env.example @@ -31,8 +31,5 @@ BAP_CLIENT_SECRET= BAP_URL= BAP_USER= BAP_PASSWORD= -BAP_SAM_TABLE= -BAP_FORMS_TABLE= -BAP_BUS_TABLE= S3_PUBLIC_BUCKET= S3_PUBLIC_REGION= diff --git a/app/server/app/routes/formio2022.js b/app/server/app/routes/formio2022.js index 0da1cbe6..7aa73bc7 100644 --- a/app/server/app/routes/formio2022.js +++ b/app/server/app/routes/formio2022.js @@ -13,24 +13,31 @@ const { verifyMongoObjectId, } = require("../middleware"); const { - getBapFormSubmissionsStatuses, - getBapDataForPRF, - getBapDataForCRF, + getBapDataFor2022CRF, checkFormSubmissionPeriodAndBapStatus, } = require("../utilities/bap"); const { uploadS3FileMetadata, downloadS3FileMetadata, + // fetchFRFSubmissions, createFRFSubmission, fetchFRFSubmission, updateFRFSubmission, + // + fetchPRFSubmissions, + createPRFSubmission, + fetchPRFSubmission, + updatePRFSubmission, + deletePRFSubmission, + // + fetchCRFSubmissions, } = require("../utilities/formio"); const log = require("../utilities/logger"); -const formioPRFUrl = formUrl["2022"].prf; const formioCRFUrl = formUrl["2022"].crf; +const rebateYear = "2022"; const router = express.Router(); router.use(ensureAuthenticated); @@ -40,7 +47,7 @@ router.get( "/s3/:formType/:mongoId/:comboKey/storage/s3", storeBapComboKeys, (req, res) => { - downloadS3FileMetadata({ rebateYear: "2022", req, res }); + downloadS3FileMetadata({ rebateYear, req, res }); }, ); @@ -49,18 +56,18 @@ router.post( "/s3/:formType/:mongoId/:comboKey/storage/s3", storeBapComboKeys, (req, res) => { - uploadS3FileMetadata({ rebateYear: "2022", req, res }); + uploadS3FileMetadata({ rebateYear, req, res }); }, ); // --- get user's 2022 FRF submissions from Formio router.get("/frf-submissions", storeBapComboKeys, (req, res) => { - fetchFRFSubmissions({ rebateYear: "2022", req, res }); + fetchFRFSubmissions({ rebateYear, req, res }); }); // --- post a new 2022 FRF submission to Formio router.post("/frf-submission", storeBapComboKeys, (req, res) => { - createFRFSubmission({ rebateYear: "2022", req, res }); + createFRFSubmission({ rebateYear, req, res }); }); // --- get an existing 2022 FRF's schema and submission data from Formio @@ -69,7 +76,7 @@ router.get( verifyMongoObjectId, storeBapComboKeys, (req, res) => { - fetchFRFSubmission({ rebateYear: "2022", req, res }); + fetchFRFSubmission({ rebateYear, req, res }); }, ); @@ -79,379 +86,38 @@ router.post( verifyMongoObjectId, storeBapComboKeys, (req, res) => { - updateFRFSubmission({ rebateYear: "2022", req, res }); + updateFRFSubmission({ rebateYear, req, res }); }, ); // --- get user's 2022 PRF submissions from Formio router.get("/prf-submissions", storeBapComboKeys, (req, res) => { - const { bapComboKeys } = req; - - const submissionsUrl = - `${formioPRFUrl}/submission` + - `?sort=-modified` + - `&limit=1000000` + - `&data.bap_hidden_entity_combo_key=${bapComboKeys.join( - "&data.bap_hidden_entity_combo_key=", - )}`; - - axiosFormio(req) - .get(submissionsUrl) - .then((axiosRes) => axiosRes.data) - .then((submissions) => res.json(submissions)) - .catch((error) => { - // NOTE: error is logged in axiosFormio response interceptor - const errorStatus = error.response?.status || 500; - const errorMessage = `Error getting Formio Payment Request form submissions.`; - return res.status(errorStatus).json({ message: errorMessage }); - }); + fetchPRFSubmissions({ rebateYear, req, res }); }); // --- post a new 2022 PRF submission to Formio router.post("/prf-submission", storeBapComboKeys, (req, res) => { - const { bapComboKeys, body } = req; - const { mail } = req.user; - const { - email, - title, - name, - entity, - comboKey, - rebateId, - frfReviewItemId, - frfFormModified, - } = body; - - if (!submissionPeriodOpen["2022"].prf) { - const errorStatus = 400; - const errorMessage = `CSB Payment Request form enrollment period is closed.`; - return res.status(errorStatus).json({ message: errorMessage }); - } - - if (!bapComboKeys.includes(comboKey)) { - const logMessage = - `User with email '${mail}' attempted to post a new PRF submission ` + - `without a matching BAP combo key.`; - log({ level: "error", message: logMessage, req }); - - const errorStatus = 401; - const errorMessage = `Unauthorized.`; - return res.status(errorStatus).json({ message: errorMessage }); - } - - const { - UNIQUE_ENTITY_ID__c, - ENTITY_EFT_INDICATOR__c, - ELEC_BUS_POC_EMAIL__c, - ALT_ELEC_BUS_POC_EMAIL__c, - GOVT_BUS_POC_EMAIL__c, - ALT_GOVT_BUS_POC_EMAIL__c, - } = entity; - - return getBapDataForPRF(req, frfReviewItemId) - .then(({ frfRecordQuery, busRecordsQuery }) => { - const { - CSB_NCES_ID__c, - Primary_Applicant__r, - Alternate_Applicant__r, - Applicant_Organization__r, - CSB_School_District__r, - Fleet_Name__c, - School_District_Prioritized__c, - Total_Rebate_Funds_Requested__c, - Total_Infrastructure_Funds__c, - } = frfRecordQuery[0]; - - const busInfo = busRecordsQuery.map((record) => ({ - busNum: record.Rebate_Item_num__c, - oldBusNcesDistrictId: CSB_NCES_ID__c, - oldBusVin: record.CSB_VIN__c, - oldBusModelYear: record.CSB_Model_Year__c, - oldBusFuelType: record.CSB_Fuel_Type__c, - newBusFuelType: record.CSB_Replacement_Fuel_Type__c, - hidden_bap_max_rebate: record.CSB_Funds_Requested__c, - })); - - /** - * NOTE: `purchaseOrders` is initialized as an empty array to fix some - * issue with the field being changed to an object when the form loads - */ - const submission = { - data: { - bap_hidden_entity_combo_key: comboKey, - hidden_application_form_modified: frfFormModified, - hidden_current_user_email: email, - hidden_current_user_title: title, - hidden_current_user_name: name, - hidden_sam_uei: UNIQUE_ENTITY_ID__c, - hidden_sam_efti: ENTITY_EFT_INDICATOR__c || "0000", - hidden_sam_elec_bus_poc_email: ELEC_BUS_POC_EMAIL__c, - hidden_sam_alt_elec_bus_poc_email: ALT_ELEC_BUS_POC_EMAIL__c, - hidden_sam_govt_bus_poc_email: GOVT_BUS_POC_EMAIL__c, - hidden_sam_alt_govt_bus_poc_email: ALT_GOVT_BUS_POC_EMAIL__c, - hidden_bap_rebate_id: rebateId, - hidden_bap_district_id: CSB_NCES_ID__c, - hidden_bap_primary_name: Primary_Applicant__r?.Name, - hidden_bap_primary_title: Primary_Applicant__r?.Title, - hidden_bap_primary_phone_number: Primary_Applicant__r?.Phone, - hidden_bap_primary_email: Primary_Applicant__r?.Email, - hidden_bap_alternate_name: Alternate_Applicant__r?.Name || "", - hidden_bap_alternate_title: Alternate_Applicant__r?.Title || "", - hidden_bap_alternate_phone_number: Alternate_Applicant__r?.Phone || "", // prettier-ignore - hidden_bap_alternate_email: Alternate_Applicant__r?.Email || "", - hidden_bap_org_name: Applicant_Organization__r?.Name, - hidden_bap_district_name: CSB_School_District__r?.Name, - hidden_bap_fleet_name: Fleet_Name__c, - hidden_bap_prioritized: School_District_Prioritized__c, - hidden_bap_requested_funds: Total_Rebate_Funds_Requested__c, - hidden_bap_infra_max_rebate: Total_Infrastructure_Funds__c, - busInfo, - purchaseOrders: [], - }, - /** Add custom metadata to track formio submissions from wrapper. */ - metadata: { - ...formioCSBMetadata, - }, - state: "draft", - }; - - axiosFormio(req) - .post(`${formioPRFUrl}/submission`, submission) - .then((axiosRes) => axiosRes.data) - .then((submission) => res.json(submission)) - .catch((error) => { - // NOTE: error is logged in axiosFormio response interceptor - const errorStatus = error.response?.status || 500; - const errorMessage = `Error posting Formio Payment Request form submission.`; - return res.status(errorStatus).json({ message: errorMessage }); - }); - }) - .catch((error) => { - // NOTE: logged in bap verifyBapConnection - const errorStatus = 500; - const errorMessage = `Error getting data for a new Payment Request form submission from the BAP.`; - return res.status(errorStatus).json({ message: errorMessage }); - }); + createPRFSubmission({ rebateYear, req, res }); }); // --- get an existing 2022 PRF's schema and submission data from Formio router.get("/prf-submission/:rebateId", storeBapComboKeys, async (req, res) => { - const { bapComboKeys } = req; - const { mail } = req.user; - const { rebateId } = req.params; // CSB Rebate ID (6 digits) - - const matchedPRFSubmissions = - `${formioPRFUrl}/submission` + - `?data.hidden_bap_rebate_id=${rebateId}` + - `&select=_id,data.bap_hidden_entity_combo_key`; - - Promise.all([ - axiosFormio(req).get(matchedPRFSubmissions), - axiosFormio(req).get(formioPRFUrl), - ]) - .then((axiosResponses) => axiosResponses.map((axiosRes) => axiosRes.data)) - .then(([submissions, schema]) => { - const submission = submissions[0]; - const mongoId = submission._id; - const comboKey = submission.data.bap_hidden_entity_combo_key; - - if (!bapComboKeys.includes(comboKey)) { - const logMessage = - `User with email '${mail}' attempted to access PRF submission '${rebateId}' ` + - `that they do not have access to.`; - log({ level: "warn", message: logMessage, req }); - - return res.json({ - userAccess: false, - formSchema: null, - submission: null, - }); - } - - /** NOTE: verifyMongoObjectId */ - if (mongoId && !ObjectId.isValid(mongoId)) { - const errorStatus = 400; - const errorMessage = `MongoDB ObjectId validation error for: '${mongoId}'.`; - return res.status(errorStatus).json({ message: errorMessage }); - } - - /** - * NOTE: We can't just use the returned submission data here because - * Formio returns the string literal 'YES' instead of a base64 encoded - * image string for signature fields when you query for all submissions - * matching on a field's value (`/submission?data.hidden_bap_rebate_id=${rebateId}`). - * We need to query for a specific submission (e.g. `/submission/${mongoId}`), - * to have Formio return the correct signature field data. - */ - axiosFormio(req) - .get(`${formioPRFUrl}/submission/${mongoId}`) - .then((axiosRes) => axiosRes.data) - .then((submission) => { - return res.json({ - userAccess: true, - formSchema: { url: formioPRFUrl, json: schema }, - submission, - }); - }); - }) - .catch((error) => { - // NOTE: error is logged in axiosFormio response interceptor - const errorStatus = error.response?.status || 500; - const errorMessage = `Error getting Formio Payment Request form submission '${rebateId}'.`; - return res.status(errorStatus).json({ message: errorMessage }); - }); + fetchPRFSubmission({ rebateYear, req, res }); }); // --- post an update to an existing draft 2022 PRF submission to Formio router.post("/prf-submission/:rebateId", storeBapComboKeys, (req, res) => { - const { bapComboKeys, body } = req; - const { mail } = req.user; - const { rebateId } = req.params; // CSB Rebate ID (6 digits) - const { mongoId, submission } = body; - const comboKey = submission.data?.bap_hidden_entity_combo_key; - - checkFormSubmissionPeriodAndBapStatus({ - rebateYear: "2022", - formType: "prf", - mongoId, - comboKey, - req, - }) - .then(() => { - if (!bapComboKeys.includes(comboKey)) { - const logMessage = - `User with email '${mail}' attempted to update PRF submission '${rebateId}' ` + - `without a matching BAP combo key.`; - log({ level: "error", message: logMessage, req }); - - const errorStatus = 401; - const errorMessage = `Unauthorized.`; - return res.status(errorStatus).json({ message: errorMessage }); - } - - /** NOTE: verifyMongoObjectId */ - if (mongoId && !ObjectId.isValid(mongoId)) { - const errorStatus = 400; - const errorMessage = `MongoDB ObjectId validation error for: '${mongoId}'.`; - return res.status(errorStatus).json({ message: errorMessage }); - } - - /** Add custom metadata to track formio submissions from wrapper. */ - submission.metadata = { - ...submission.metadata, - ...formioCSBMetadata, - }; - - axiosFormio(req) - .put(`${formioPRFUrl}/submission/${mongoId}`, submission) - .then((axiosRes) => axiosRes.data) - .then((submission) => res.json(submission)) - .catch((error) => { - // NOTE: error is logged in axiosFormio response interceptor - const errorStatus = error.response?.status || 500; - const errorMessage = `Error updating Formio Payment Request form submission '${rebateId}'.`; - return res.status(errorStatus).json({ message: errorMessage }); - }); - }) - .catch((error) => { - const logMessage = - `User with email '${mail}' attempted to update PRF submission '${rebateId}' ` + - `when the CSB PRF enrollment period was closed.`; - log({ level: "error", message: logMessage, req }); - - const errorStatus = 400; - const errorMessage = `CSB Payment Request form enrollment period is closed.`; - return res.status(errorStatus).json({ message: errorMessage }); - }); + updatePRFSubmission({ rebateYear, req, res }); }); // --- delete an existing 2022 PRF submission from Formio router.post("/delete-prf-submission", storeBapComboKeys, (req, res) => { - const { bapComboKeys, body } = req; - const { mail } = req.user; - const { mongoId, rebateId, comboKey } = body; - - // verify post data includes one of user's BAP combo keys - if (!bapComboKeys.includes(comboKey)) { - const logMessage = - `User with email '${mail}' attempted to delete PRF submission '${rebateId}' ` + - `without a matching BAP combo key.`; - log({ level: "error", message: logMessage, req }); - - const errorStatus = 401; - const errorMessage = `Unauthorized.`; - return res.status(errorStatus).json({ message: errorMessage }); - } - - /** - * ensure the BAP status of the corresponding FRF submission is "Edits - * Requested" before deleting the FRF submission from Formio - */ - getBapFormSubmissionsStatuses(req, req.bapComboKeys) - .then((submissions) => { - const frf = submissions.find((submission) => { - return ( - submission.Parent_Rebate_ID__c === rebateId && - submission.Record_Type_Name__c === "CSB Funding Request" - ); - }); - - const frfNeedsEdits = - frf?.Parent_CSB_Rebate__r.CSB_Funding_Request_Status__c === - "Edits Requested"; - - if (!frfNeedsEdits) { - const errorStatus = 400; - const errorMessage = `Application form submission '${mongoId}' does not need edits.`; - return res.status(errorStatus).json({ message: errorMessage }); - } - - axiosFormio(req) - .delete(`${formioPRFUrl}/submission/${mongoId}`) - .then((axiosRes) => axiosRes.data) - .then((response) => { - const logMessage = `User with email '${mail}' successfully deleted PRF submission '${rebateId}'.`; - log({ level: "info", message: logMessage, req }); - - res.json(response); - }) - .catch((error) => { - // NOTE: error is logged in axiosFormio response interceptor - const errorStatus = error.response?.status || 500; - const errorMessage = `Error deleting Formio Payment Request form submission '${rebateId}'.`; - return res.status(errorStatus).json({ message: errorMessage }); - }); - }) - .catch((error) => { - // NOTE: logged in bap verifyBapConnection - const errorStatus = 500; - const errorMessage = `Error getting form submissions statuses from the BAP.`; - return res.status(errorStatus).json({ message: errorMessage }); - }); + deletePRFSubmission({ rebateYear, req, res }); }); // --- get user's 2022 CRF submissions from Formio router.get("/crf-submissions", storeBapComboKeys, (req, res) => { - const { bapComboKeys } = req; - - const submissionsUrl = - `${formioCRFUrl}/submission` + - `?sort=-modified` + - `&limit=1000000` + - `&data.bap_hidden_entity_combo_key=${bapComboKeys.join( - "&data.bap_hidden_entity_combo_key=", - )}`; - - axiosFormio(req) - .get(submissionsUrl) - .then((axiosRes) => axiosRes.data) - .then((submissions) => res.json(submissions)) - .catch((error) => { - // NOTE: error is logged in axiosFormio response interceptor - const errorStatus = error.response?.status || 500; - const errorMessage = `Error getting Formio Close Out form submissions.`; - return res.status(errorStatus).json({ message: errorMessage }); - }); + fetchCRFSubmissions({ rebateYear, req, res }); }); // --- post a new 2022 CRF submission to Formio @@ -496,138 +162,140 @@ router.post("/crf-submission", storeBapComboKeys, (req, res) => { ALT_GOVT_BUS_POC_EMAIL__c, } = entity; - return getBapDataForCRF(req, frfReviewItemId, prfReviewItemId) - .then(({ frfRecordQuery, prfRecordQuery, busRecordsQuery }) => { - const { - Fleet_Name__c, - Fleet_Street_Address__c, - Fleet_City__c, - Fleet_State__c, - Fleet_Zip__c, - Fleet_Contact_Name__c, - Fleet_Contact_Title__c, - Fleet_Contact_Phone__c, - Fleet_Contact_Email__c, - School_District_Contact__r, - } = frfRecordQuery[0]; - - const { - CSB_NCES_ID__c, - Primary_Applicant__r, - Alternate_Applicant__r, - Applicant_Organization__r, - CSB_School_District__r, - School_District_Prioritized__c, - Total_Rebate_Funds_Requested_PO__c, - Total_Bus_And_Infrastructure_Rebate__c, - Total_Infrastructure_Funds__c, - Num_Of_Buses_Requested_From_Application__c, - Total_Price_All_Buses__c, - Total_Bus_Rebate_Amount__c, - Total_All_Eligible_Infrastructure_Costs__c, - Total_Infrastructure_Rebate__c, - Total_Level_2_Charger_Costs__c, - Total_DC_Fast_Charger_Costs__c, - Total_Other_Infrastructure_Costs__c, - } = prfRecordQuery[0]; - - const busInfo = busRecordsQuery.map((record) => ({ - busNum: record.Rebate_Item_num__c, - oldBusNcesDistrictId: CSB_NCES_ID__c, - oldBusVin: record.CSB_VIN__c, - oldBusModelYear: record.CSB_Model_Year__c, - oldBusFuelType: record.CSB_Fuel_Type__c, - oldBusEstimatedRemainingLife: record.Old_Bus_Estimated_Remaining_Life__c, // prettier-ignore - oldBusExclude: record.Old_Bus_Exclude__c, - hidden_prf_oldBusExclude: record.Old_Bus_Exclude__c, - newBusDealer: record.Related_Line_Item__r?.Vendor_Name__c, - newBusFuelType: record.New_Bus_Fuel_Type__c, - hidden_prf_newBusFuelType: record.New_Bus_Fuel_Type__c, - newBusMake: record.New_Bus_Make__c, - hidden_prf_newBusMake: record.New_Bus_Make__c, - newBusMakeOther: record.CSB_Manufacturer_if_Other__c, - hidden_prf_newBusMakeOther: record.CSB_Manufacturer_if_Other__c, - newBusModel: record.New_Bus_Model__c, - hidden_prf_newBusModel: record.New_Bus_Model__c, - newBusModelYear: record.New_Bus_Model_Year__c, - hidden_prf_newBusModelYear: record.New_Bus_Model_Year__c, - newBusGvwr: record.New_Bus_GVWR__c, - hidden_prf_newBusGvwr: record.New_Bus_GVWR__c, - newBusPurchasePrice: record.New_Bus_Purchase_Price__c, - hidden_prf_newBusPurchasePrice: record.New_Bus_Purchase_Price__c, - hidden_prf_rebate: record.New_Bus_Rebate_Amount__c, - })); - - const submission = { - data: { - bap_hidden_entity_combo_key: comboKey, - hidden_prf_modified: prfModified, - hidden_current_user_email: email, - hidden_current_user_title: title, - hidden_current_user_name: name, - hidden_bap_rebate_id: rebateId, - hidden_sam_uei: UNIQUE_ENTITY_ID__c, - hidden_sam_efti: ENTITY_EFT_INDICATOR__c || "0000", - hidden_sam_elec_bus_poc_email: ELEC_BUS_POC_EMAIL__c, - hidden_sam_alt_elec_bus_poc_email: ALT_ELEC_BUS_POC_EMAIL__c, - hidden_sam_govt_bus_poc_email: GOVT_BUS_POC_EMAIL__c, - hidden_sam_alt_govt_bus_poc_email: ALT_GOVT_BUS_POC_EMAIL__c, - hidden_bap_district_id: CSB_NCES_ID__c, - hidden_bap_district_name: CSB_School_District__r?.Name, - hidden_bap_primary_fname: Primary_Applicant__r?.FirstName, - hidden_bap_primary_lname: Primary_Applicant__r?.LastName, - hidden_bap_primary_title: Primary_Applicant__r?.Title, - hidden_bap_primary_phone_number: Primary_Applicant__r?.Phone, - hidden_bap_primary_email: Primary_Applicant__r?.Email, - hidden_bap_alternate_fname: Alternate_Applicant__r?.FirstName || "", - hidden_bap_alternate_lname: Alternate_Applicant__r?.LastName || "", - hidden_bap_alternate_title: Alternate_Applicant__r?.Title || "", - hidden_bap_alternate_phone_number: Alternate_Applicant__r?.Phone || "", // prettier-ignore - hidden_bap_alternate_email: Alternate_Applicant__r?.Email || "", - hidden_bap_org_name: Applicant_Organization__r?.Name, - hidden_bap_fleet_name: Fleet_Name__c, - hidden_bap_fleet_address: Fleet_Street_Address__c, - hidden_bap_fleet_city: Fleet_City__c, - hidden_bap_fleet_state: Fleet_State__c, - hidden_bap_fleet_zip: Fleet_Zip__c, - hidden_bap_fleet_contact_name: Fleet_Contact_Name__c, - hidden_bap_fleet_contact_title: Fleet_Contact_Title__c, - hidden_bap_fleet_phone: Fleet_Contact_Phone__c, - hidden_bap_fleet_email: Fleet_Contact_Email__c, - hidden_bap_prioritized: School_District_Prioritized__c, - hidden_bap_requested_funds: Total_Rebate_Funds_Requested_PO__c, - hidden_bap_received_funds: Total_Bus_And_Infrastructure_Rebate__c, - hidden_bap_prf_infra_max_rebate: Total_Infrastructure_Funds__c, - hidden_bap_buses_requested_app: Num_Of_Buses_Requested_From_Application__c, // prettier-ignore - hidden_bap_total_bus_costs_prf: Total_Price_All_Buses__c, - hidden_bap_total_bus_rebate_received: Total_Bus_Rebate_Amount__c, - hidden_bap_total_infra_costs_prf: Total_All_Eligible_Infrastructure_Costs__c, // prettier-ignore - hidden_bap_total_infra_rebate_received: Total_Infrastructure_Rebate__c, // prettier-ignore - hidden_bap_total_infra_level2_charger: Total_Level_2_Charger_Costs__c, - hidden_bap_total_infra_dc_fast_charger: Total_DC_Fast_Charger_Costs__c, // prettier-ignore - hidden_bap_total_infra_other_costs: Total_Other_Infrastructure_Costs__c, // prettier-ignore - hidden_bap_district_contact_fname: School_District_Contact__r?.FirstName, // prettier-ignore - hidden_bap_district_contact_lname: School_District_Contact__r?.LastName, // prettier-ignore - busInfo, - }, - /** Add custom metadata to track formio submissions from wrapper. */ - metadata: { - ...formioCSBMetadata, - }, - state: "draft", - }; - - axiosFormio(req) - .post(`${formioCRFUrl}/submission`, submission) - .then((axiosRes) => axiosRes.data) - .then((submission) => res.json(submission)) - .catch((error) => { - // NOTE: error is logged in axiosFormio response interceptor - const errorStatus = error.response?.status || 500; - const errorMessage = `Error posting Formio Close Out form submission.`; - return res.status(errorStatus).json({ message: errorMessage }); - }); - }) + return getBapDataFor2022CRF(req, frfReviewItemId, prfReviewItemId) + .then( + ({ frf2022RecordQuery, prf2022RecordQuery, prf2022busRecordsQuery }) => { + const { + Fleet_Name__c, + Fleet_Street_Address__c, + Fleet_City__c, + Fleet_State__c, + Fleet_Zip__c, + Fleet_Contact_Name__c, + Fleet_Contact_Title__c, + Fleet_Contact_Phone__c, + Fleet_Contact_Email__c, + School_District_Contact__r, + } = frf2022RecordQuery[0]; + + const { + CSB_NCES_ID__c, + Primary_Applicant__r, + Alternate_Applicant__r, + Applicant_Organization__r, + CSB_School_District__r, + School_District_Prioritized__c, + Total_Rebate_Funds_Requested_PO__c, + Total_Bus_And_Infrastructure_Rebate__c, + Total_Infrastructure_Funds__c, + Num_Of_Buses_Requested_From_Application__c, + Total_Price_All_Buses__c, + Total_Bus_Rebate_Amount__c, + Total_All_Eligible_Infrastructure_Costs__c, + Total_Infrastructure_Rebate__c, + Total_Level_2_Charger_Costs__c, + Total_DC_Fast_Charger_Costs__c, + Total_Other_Infrastructure_Costs__c, + } = prf2022RecordQuery[0]; + + const busInfo = prf2022busRecordsQuery.map((record) => ({ + busNum: record.Rebate_Item_num__c, + oldBusNcesDistrictId: CSB_NCES_ID__c, + oldBusVin: record.CSB_VIN__c, + oldBusModelYear: record.CSB_Model_Year__c, + oldBusFuelType: record.CSB_Fuel_Type__c, + oldBusEstimatedRemainingLife: record.Old_Bus_Estimated_Remaining_Life__c, // prettier-ignore + oldBusExclude: record.Old_Bus_Exclude__c, + hidden_prf_oldBusExclude: record.Old_Bus_Exclude__c, + newBusDealer: record.Related_Line_Item__r?.Vendor_Name__c, + newBusFuelType: record.New_Bus_Fuel_Type__c, + hidden_prf_newBusFuelType: record.New_Bus_Fuel_Type__c, + newBusMake: record.New_Bus_Make__c, + hidden_prf_newBusMake: record.New_Bus_Make__c, + newBusMakeOther: record.CSB_Manufacturer_if_Other__c, + hidden_prf_newBusMakeOther: record.CSB_Manufacturer_if_Other__c, + newBusModel: record.New_Bus_Model__c, + hidden_prf_newBusModel: record.New_Bus_Model__c, + newBusModelYear: record.New_Bus_Model_Year__c, + hidden_prf_newBusModelYear: record.New_Bus_Model_Year__c, + newBusGvwr: record.New_Bus_GVWR__c, + hidden_prf_newBusGvwr: record.New_Bus_GVWR__c, + newBusPurchasePrice: record.New_Bus_Purchase_Price__c, + hidden_prf_newBusPurchasePrice: record.New_Bus_Purchase_Price__c, + hidden_prf_rebate: record.New_Bus_Rebate_Amount__c, + })); + + const submission = { + data: { + bap_hidden_entity_combo_key: comboKey, + hidden_prf_modified: prfModified, + hidden_current_user_email: email, + hidden_current_user_title: title, + hidden_current_user_name: name, + hidden_bap_rebate_id: rebateId, + hidden_sam_uei: UNIQUE_ENTITY_ID__c, + hidden_sam_efti: ENTITY_EFT_INDICATOR__c || "0000", + hidden_sam_elec_bus_poc_email: ELEC_BUS_POC_EMAIL__c, + hidden_sam_alt_elec_bus_poc_email: ALT_ELEC_BUS_POC_EMAIL__c, + hidden_sam_govt_bus_poc_email: GOVT_BUS_POC_EMAIL__c, + hidden_sam_alt_govt_bus_poc_email: ALT_GOVT_BUS_POC_EMAIL__c, + hidden_bap_district_id: CSB_NCES_ID__c, + hidden_bap_district_name: CSB_School_District__r?.Name, + hidden_bap_primary_fname: Primary_Applicant__r?.FirstName, + hidden_bap_primary_lname: Primary_Applicant__r?.LastName, + hidden_bap_primary_title: Primary_Applicant__r?.Title, + hidden_bap_primary_phone_number: Primary_Applicant__r?.Phone, + hidden_bap_primary_email: Primary_Applicant__r?.Email, + hidden_bap_alternate_fname: Alternate_Applicant__r?.FirstName || "", + hidden_bap_alternate_lname: Alternate_Applicant__r?.LastName || "", + hidden_bap_alternate_title: Alternate_Applicant__r?.Title || "", + hidden_bap_alternate_phone_number: Alternate_Applicant__r?.Phone || "", // prettier-ignore + hidden_bap_alternate_email: Alternate_Applicant__r?.Email || "", + hidden_bap_org_name: Applicant_Organization__r?.Name, + hidden_bap_fleet_name: Fleet_Name__c, + hidden_bap_fleet_address: Fleet_Street_Address__c, + hidden_bap_fleet_city: Fleet_City__c, + hidden_bap_fleet_state: Fleet_State__c, + hidden_bap_fleet_zip: Fleet_Zip__c, + hidden_bap_fleet_contact_name: Fleet_Contact_Name__c, + hidden_bap_fleet_contact_title: Fleet_Contact_Title__c, + hidden_bap_fleet_phone: Fleet_Contact_Phone__c, + hidden_bap_fleet_email: Fleet_Contact_Email__c, + hidden_bap_prioritized: School_District_Prioritized__c, + hidden_bap_requested_funds: Total_Rebate_Funds_Requested_PO__c, + hidden_bap_received_funds: Total_Bus_And_Infrastructure_Rebate__c, + hidden_bap_prf_infra_max_rebate: Total_Infrastructure_Funds__c, + hidden_bap_buses_requested_app: Num_Of_Buses_Requested_From_Application__c, // prettier-ignore + hidden_bap_total_bus_costs_prf: Total_Price_All_Buses__c, + hidden_bap_total_bus_rebate_received: Total_Bus_Rebate_Amount__c, + hidden_bap_total_infra_costs_prf: Total_All_Eligible_Infrastructure_Costs__c, // prettier-ignore + hidden_bap_total_infra_rebate_received: Total_Infrastructure_Rebate__c, // prettier-ignore + hidden_bap_total_infra_level2_charger: Total_Level_2_Charger_Costs__c, // prettier-ignore + hidden_bap_total_infra_dc_fast_charger: Total_DC_Fast_Charger_Costs__c, // prettier-ignore + hidden_bap_total_infra_other_costs: Total_Other_Infrastructure_Costs__c, // prettier-ignore + hidden_bap_district_contact_fname: School_District_Contact__r?.FirstName, // prettier-ignore + hidden_bap_district_contact_lname: School_District_Contact__r?.LastName, // prettier-ignore + busInfo, + }, + /** Add custom metadata to track formio submissions from wrapper. */ + metadata: { + ...formioCSBMetadata, + }, + state: "draft", + }; + + axiosFormio(req) + .post(`${formioCRFUrl}/submission`, submission) + .then((axiosRes) => axiosRes.data) + .then((submission) => res.json(submission)) + .catch((error) => { + // NOTE: error is logged in axiosFormio response interceptor + const errorStatus = error.response?.status || 500; + const errorMessage = `Error posting Formio Close Out form submission.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); + }, + ) .catch((error) => { // NOTE: logged in bap verifyBapConnection const errorStatus = 500; @@ -713,7 +381,7 @@ router.post("/crf-submission/:rebateId", storeBapComboKeys, (req, res) => { const comboKey = submission.data?.bap_hidden_entity_combo_key; checkFormSubmissionPeriodAndBapStatus({ - rebateYear: "2022", + rebateYear, formType: "crf", mongoId, comboKey, diff --git a/app/server/app/routes/formio2023.js b/app/server/app/routes/formio2023.js index 2de8ea34..a8e46dca 100644 --- a/app/server/app/routes/formio2023.js +++ b/app/server/app/routes/formio2023.js @@ -1,33 +1,27 @@ const express = require("express"); -const ObjectId = require("mongodb").ObjectId; // --- -const { - axiosFormio, - formUrl, - submissionPeriodOpen, - formioCSBMetadata, -} = require("../config/formio"); const { ensureAuthenticated, storeBapComboKeys, verifyMongoObjectId, } = require("../middleware"); -const { - getBapFormSubmissionsStatuses, - getBapDataForPRF, - getBapDataForCRF, - checkFormSubmissionPeriodAndBapStatus, -} = require("../utilities/bap"); const { uploadS3FileMetadata, downloadS3FileMetadata, + // fetchFRFSubmissions, createFRFSubmission, fetchFRFSubmission, updateFRFSubmission, + // + fetchPRFSubmissions, + createPRFSubmission, + fetchPRFSubmission, + updatePRFSubmission, + deletePRFSubmission, } = require("../utilities/formio"); -const log = require("../utilities/logger"); +const rebateYear = "2023"; const router = express.Router(); router.use(ensureAuthenticated); @@ -37,7 +31,7 @@ router.get( "/s3/:formType/:mongoId/:comboKey/storage/s3", storeBapComboKeys, (req, res) => { - downloadS3FileMetadata({ rebateYear: "2023", req, res }); + downloadS3FileMetadata({ rebateYear, req, res }); }, ); @@ -46,18 +40,18 @@ router.post( "/s3/:formType/:mongoId/:comboKey/storage/s3", storeBapComboKeys, (req, res) => { - uploadS3FileMetadata({ rebateYear: "2023", req, res }); + uploadS3FileMetadata({ rebateYear, req, res }); }, ); // --- get user's 2023 FRF submissions from Formio router.get("/frf-submissions", storeBapComboKeys, (req, res) => { - fetchFRFSubmissions({ rebateYear: "2023", req, res }); + fetchFRFSubmissions({ rebateYear, req, res }); }); // --- post a new 2023 FRF submission to Formio router.post("/frf-submission", storeBapComboKeys, (req, res) => { - createFRFSubmission({ rebateYear: "2023", req, res }); + createFRFSubmission({ rebateYear, req, res }); }); // --- get an existing 2023 FRF's schema and submission data from Formio @@ -66,7 +60,7 @@ router.get( verifyMongoObjectId, storeBapComboKeys, (req, res) => { - fetchFRFSubmission({ rebateYear: "2023", req, res }); + fetchFRFSubmission({ rebateYear, req, res }); }, ); @@ -76,25 +70,33 @@ router.post( verifyMongoObjectId, storeBapComboKeys, (req, res) => { - updateFRFSubmission({ rebateYear: "2023", req, res }); + updateFRFSubmission({ rebateYear, req, res }); }, ); // --- get user's 2023 PRF submissions from Formio router.get("/prf-submissions", storeBapComboKeys, (req, res) => { - // TODO - res.json([]); + fetchPRFSubmissions({ rebateYear, req, res }); }); // --- post a new 2023 PRF submission to Formio +router.post("/prf-submission", storeBapComboKeys, (req, res) => { + createPRFSubmission({ rebateYear, req, res }); +}); // --- get an existing 2023 PRF's schema and submission data from Formio +router.get("/prf-submission/:rebateId", storeBapComboKeys, async (req, res) => { + fetchPRFSubmission({ rebateYear, req, res }); +}); // --- post an update to an existing draft 2023 PRF submission to Formio +router.post("/prf-submission/:rebateId", storeBapComboKeys, (req, res) => { + updatePRFSubmission({ rebateYear, req, res }); +}); // --- delete an existing 2023 PRF submission from Formio router.post("/delete-prf-submission", storeBapComboKeys, (req, res) => { - // TODO + deletePRFSubmission({ rebateYear, req, res }); }); // --- get user's 2022 CRF submissions from Formio diff --git a/app/server/app/utilities/bap.js b/app/server/app/utilities/bap.js index 7bec316e..aa2e53f2 100644 --- a/app/server/app/utilities/bap.js +++ b/app/server/app/utilities/bap.js @@ -58,7 +58,7 @@ const { submissionPeriodOpen } = require("../config/formio"); */ /** - * @typedef {Object} BapDataForPRF + * @typedef {Object} BapDataFor2022PRF * @property {{ * Id: string * UEI_EFTI_Combo_Key__c: string @@ -85,7 +85,7 @@ const { submissionPeriodOpen } = require("../config/formio"); * School_District_Prioritized__c: string * Total_Rebate_Funds_Requested__c: string * Total_Infrastructure_Funds__c: string - * }[]} frfRecordQuery + * }[]} frf2022RecordQuery * @property {{ * Rebate_Item_num__c: string * CSB_VIN__c: string @@ -93,7 +93,7 @@ const { submissionPeriodOpen } = require("../config/formio"); * CSB_Fuel_Type__c: string * CSB_Replacement_Fuel_Type__c: string * CSB_Funds_Requested__c: string - * }[]} busRecordsQuery + * }[]} frf2022BusRecordsQuery * @property {{ * type: string * url: string @@ -101,7 +101,82 @@ const { submissionPeriodOpen } = require("../config/formio"); */ /** - * @typedef {Object} BapDataForForCRF + * @typedef {Object} BapDataFor2023PRF + * @property {{ + * Id: string + * Primary_Applicant__r: { + * FirstName: string + * LastName: string + * Title: string + * Email: string + * Phone: string + * } | null + * Alternate_Applicant__r: { + * FirstName: string + * LastName: string + * Title: string + * Email: string + * Phone: string + * } | null + * CSB_School_District__r: { + * Name: string + * BillingStreet: string + * BillingCity: string + * BillingState: string + * BillingPostalCode: string + * } | null + * School_District_Contact__r: { + * FirstName: string + * LastName: string + * Title: string + * Email: string + * Phone: string + * } | null + * CSB_NCES_ID__c: string + * School_District_Prioritized__c: string + * School_District_Poverty_Rate__c: string + * Prioritized_as_High_Need__c: string + * Prioritized_as_Tribal__c: string + * Prioritized_as_Rural__c: string + * }[]} frf2023RecordQuery + * @property {{ + * Id: string + * Rebate_Item_num__c: string + * CSB_VIN__c: string + * CSB_Fuel_Type__c: string + * CSB_GVWR__c: string + * Old_Bus_Odometer_miles__c: string + * CSB_Model__c: string + * CSB_Model_Year__c: string + * CSB_Manufacturer__c: string + * CSB_Manufacturer_if_Other__c: string + * CSB_Annual_Fuel_Consumption__c: string + * Annual_Mileage__c: string + * Old_Bus_Estimated_Remaining_Life__c: string + * Old_Bus_Annual_Idling_Hours__c: string + * CSB_Funds_Requested__c: string + * New_Bus_Fuel_Type__c: string + * New_Bus_GVWR__c: string + * New_Bus_ADA_Compliant__c: string + * }[]} frf2023BusRecordsQuery + * @property {{ + * Id: string + * Related_Line_Item__c: string + * Relationship_Type__c: string + * Contact_Organization_Name__c: string + * Contact__r: { + * FirstName: string + * LastName: string + * } | null + * }[]} frf2023BusRecordsContactsQueries + * @property {{ + * type: string + * url: string + * }} attributes + */ + +/** + * @typedef {Object} BapDataForFor2022CRF * @property {{ * Fleet_Name__c: string * Fleet_Street_Address__c: string @@ -116,7 +191,7 @@ const { submissionPeriodOpen } = require("../config/formio"); * FirstName: string * LastName: string * } - * }[]} frfRecordQuery + * }[]} frf2022RecordQuery * @property {{ * Id: string * UEI_EFTI_Combo_Key__c: string @@ -153,7 +228,7 @@ const { submissionPeriodOpen } = require("../config/formio"); * Total_Level_2_Charger_Costs__c: string * Total_DC_Fast_Charger_Costs__c: string * Total_Other_Infrastructure_Costs__c: string - * }[]} prfRecordQuery + * }[]} prf2022RecordQuery * @property {{ * Rebate_Item_num__c: string * CSB_VIN__c: string @@ -173,7 +248,7 @@ const { submissionPeriodOpen } = require("../config/formio"); * New_Bus_GVWR__c: string * New_Bus_Rebate_Amount__c: string * New_Bus_Purchase_Price__c: string - * }[]} busRecordsQuery + * }[]} prf2022busRecordsQuery * @property {{ * type: string * url: string @@ -187,9 +262,6 @@ const { BAP_URL, BAP_USER, BAP_PASSWORD, - BAP_SAM_TABLE, - BAP_FORMS_TABLE, - BAP_BUS_TABLE, } = process.env; /** @@ -263,7 +335,7 @@ async function queryForSamEntities(req, email) { // PHYSICAL_ADDRESS_ZIPPOSTAL_CODE__c, // PHYSICAL_ADDRESS_ZIP_CODE_4__c // FROM - // ${BAP_SAM_TABLE} + // Data_Staging__c // WHERE // ALT_ELEC_BUS_POC_EMAIL__c = '${email}' OR // GOVT_BUS_POC_EMAIL__c = '${email}' OR @@ -271,7 +343,7 @@ async function queryForSamEntities(req, email) { // ELEC_BUS_POC_EMAIL__c = '${email}'` return await bapConnection - .sobject(BAP_SAM_TABLE) + .sobject("Data_Staging__c") .find( { $or: [ @@ -331,7 +403,7 @@ async function queryForBapFormSubmissionData(req, formType, rebateId, mongoId) { /** @type {jsforce.Connection} */ const { bapConnection } = req.app.locals; - const developername = + const developerName = formType === "frf" ? "CSB_Funding_Request" : formType === "prf" @@ -340,23 +412,23 @@ async function queryForBapFormSubmissionData(req, formType, rebateId, mongoId) { ? "CSB_Closeout_Request" : null; // fallback - if (!developername) return null; + if (!developerName) return null; // `SELECT // Id // FROM - // recordtype + // RecordType // WHERE - // developername = '${developername}' AND - // sobjecttype = '${BAP_FORMS_TABLE}' + // DeveloperName = '${developerName}' AND + // SObjectType = 'Order_Request__c' // LIMIT 1` const formRecordTypeIdQuery = await bapConnection - .sobject("recordtype") + .sobject("RecordType") .find( { - developername, - sobjecttype: BAP_FORMS_TABLE, + DeveloperName: developerName, + SObjectType: "Order_Request__c", }, { // "*": 1, @@ -379,17 +451,17 @@ async function queryForBapFormSubmissionData(req, formType, rebateId, mongoId) { // Parent_CSB_Rebate__r.CSB_Payment_Request_Status__c, // Parent_CSB_Rebate__r.CSB_Closeout_Request_Status__c // FROM - // ${BAP_FORMS_TABLE} + // Order_Request__c // WHERE - // recordtypeid = '${formRecordTypeId}' AND + // RecordTypeId = '${formRecordTypeId}' AND // CSB_Form_ID__c = '${mongoId}' // Latest_Version__c = TRUE` const formRecordQuery = await bapConnection - .sobject(BAP_FORMS_TABLE) + .sobject("Order_Request__c") .find( { - recordtypeid: formRecordTypeId, + RecordTypeId: formRecordTypeId, ...(mongoId && { CSB_Form_ID__c: mongoId }), ...(rebateId && { Parent_Rebate_ID__c: rebateId }), Latest_Version__c: true, @@ -432,15 +504,15 @@ async function queryForBapFormSubmissionsStatuses(req, comboKeys) { // `SELECT // Parent_Rebate_ID__c, // FROM - // ${BAP_FORMS_TABLE} + // Order_Request__c // WHERE // (${comboKeys // .map((key) => `UEI_EFTI_Combo_Key__c = '${key}'`) // .join(" OR ")}) AND // Latest_Version__c = TRUE` - const parentRebateIdsQuery = await bapConnection - .sobject(BAP_FORMS_TABLE) + const csbRebateIdsQuery = await bapConnection + .sobject("Order_Request__c") .find( { UEI_EFTI_Combo_Key__c: { $in: comboKeys }, @@ -454,11 +526,11 @@ async function queryForBapFormSubmissionsStatuses(req, comboKeys) { .sort({ CreatedDate: -1 }) .execute(async (err, records) => ((await err) ? err : records)); - const parentRebateIds = Array.isArray(parentRebateIdsQuery) - ? parentRebateIdsQuery.map((item) => item.Parent_Rebate_ID__c) + const csbRebateIds = Array.isArray(csbRebateIdsQuery) + ? csbRebateIdsQuery.map((item) => item.Parent_Rebate_ID__c) : []; - if (parentRebateIds.length === 0) return []; + if (csbRebateIds.length === 0) return []; // `SELECT // UEI_EFTI_Combo_Key__c, @@ -472,9 +544,9 @@ async function queryForBapFormSubmissionsStatuses(req, comboKeys) { // Parent_CSB_Rebate__r.CSB_Payment_Request_Status__c, // Parent_CSB_Rebate__r.CSB_Closeout_Request_Status__c // FROM - // ${BAP_FORMS_TABLE} + // Order_Request__c // WHERE - // (${parentRebateIds + // (${csbRebateIds // .map((id) => `Parent_CSB_Rebate__r.CSB_Rebate_ID__c = '${id}'`) // .join(" OR ")}) AND // Latest_Version__c = TRUE @@ -482,10 +554,10 @@ async function queryForBapFormSubmissionsStatuses(req, comboKeys) { // CreatedDate DESC` const submissions = await bapConnection - .sobject(BAP_FORMS_TABLE) + .sobject("Order_Request__c") .find( { - "Parent_CSB_Rebate__r.CSB_Rebate_ID__c": { $in: parentRebateIds }, + "Parent_CSB_Rebate__r.CSB_Rebate_ID__c": { $in: csbRebateIds }, Latest_Version__c: true, }, { @@ -509,16 +581,16 @@ async function queryForBapFormSubmissionsStatuses(req, comboKeys) { } /** - * Uses cached JSforce connection to query the BAP for FRF submission data, for - * use in a brand new PRF submission. + * Uses cached JSforce connection to query the BAP for 2022 FRF submission data, + * for use in a brand new 2022 PRF submission. * * @param {express.Request} req * @param {string} frfReviewItemId CSB Rebate ID with the form/version ID (9 digits) - * @returns {Promise} FRF submission fields + * @returns {Promise} 2022 FRF submission fields */ -async function queryBapForPRFData(req, frfReviewItemId) { +async function queryBapFor2022PRFData(req, frfReviewItemId) { const logMessage = - `Querying the BAP for FRF submission associated with ` + + `Querying the BAP for 2022 FRF submission associated with ` + `FRF Review Item ID: '${frfReviewItemId}'.`; log({ level: "info", message: logMessage }); @@ -528,18 +600,18 @@ async function queryBapForPRFData(req, frfReviewItemId) { // `SELECT // Id // FROM - // recordtype + // RecordType // WHERE - // developername = 'CSB_Funding_Request' AND - // sobjecttype = '${BAP_FORMS_TABLE}' + // DeveloperName = 'CSB_Funding_Request' AND + // SObjectType = 'Order_Request__c' // LIMIT 1` - const frfRecordTypeIdQuery = await bapConnection - .sobject("recordtype") + const frf2022RecordTypeIdQuery = await bapConnection + .sobject("RecordType") .find( { - developername: "CSB_Funding_Request", - sobjecttype: BAP_FORMS_TABLE, + DeveloperName: "CSB_Funding_Request", + SObjectType: "Order_Request__c", }, { // "*": 1, @@ -549,7 +621,7 @@ async function queryBapForPRFData(req, frfReviewItemId) { .limit(1) .execute(async (err, records) => ((await err) ? err : records)); - const frfRecordTypeId = frfRecordTypeIdQuery["0"].Id; + const frf2022RecordTypeId = frf2022RecordTypeIdQuery["0"].Id; // `SELECT // Id, @@ -570,17 +642,17 @@ async function queryBapForPRFData(req, frfReviewItemId) { // Total_Rebate_Funds_Requested__c, // Total_Infrastructure_Funds__c // FROM - // ${BAP_FORMS_TABLE} + // Order_Request__c // WHERE - // recordtypeid = '${frfRecordTypeId}' AND + // RecordTypeId = '${frf2022RecordTypeId}' AND // CSB_Review_Item_ID__c = '${frfReviewItemId}' AND // Latest_Version__c = TRUE` - const frfRecordQuery = await bapConnection - .sobject(BAP_FORMS_TABLE) + const frf2022RecordQuery = await bapConnection + .sobject("Order_Request__c") .find( { - recordtypeid: frfRecordTypeId, + RecordTypeId: frf2022RecordTypeId, CSB_Review_Item_ID__c: frfReviewItemId, Latest_Version__c: true, }, @@ -607,23 +679,23 @@ async function queryBapForPRFData(req, frfReviewItemId) { ) .execute(async (err, records) => ((await err) ? err : records)); - const frfRecordId = frfRecordQuery["0"].Id; + const frf2022RecordId = frf2022RecordQuery["0"].Id; // `SELECT // Id // FROM - // recordtype + // RecordType // WHERE - // developername = 'CSB_Rebate_Item' AND - // sobjecttype = '${BAP_BUS_TABLE}' + // DeveloperName = 'CSB_Rebate_Item' AND + // SObjectType = 'Line_Item__c' // LIMIT 1` - const busRecordTypeIdQuery = await bapConnection - .sobject("recordtype") + const rebateItemRecordTypeIdQuery = await bapConnection + .sobject("RecordType") .find( { - developername: "CSB_Rebate_Item", - sobjecttype: BAP_BUS_TABLE, + DeveloperName: "CSB_Rebate_Item", + SObjectType: "Line_Item__c", }, { // "*": 1, @@ -633,7 +705,7 @@ async function queryBapForPRFData(req, frfReviewItemId) { .limit(1) .execute(async (err, records) => ((await err) ? err : records)); - const busRecordTypeId = busRecordTypeIdQuery["0"].Id; + const rebateItemRecordTypeId = rebateItemRecordTypeIdQuery["0"].Id; // `SELECT // Rebate_Item_num__c, @@ -643,18 +715,18 @@ async function queryBapForPRFData(req, frfReviewItemId) { // CSB_Replacement_Fuel_Type__c, // CSB_Funds_Requested__c // FROM - // ${BAP_BUS_TABLE} + // Line_Item__c // WHERE - // recordtypeid = '${busRecordTypeId}' AND - // Related_Order_Request__c = '${frfRecordId}' AND + // RecordTypeId = '${rebateItemRecordTypeId}' AND + // Related_Order_Request__c = '${frf2022RecordId}' AND // CSB_Rebate_Item_Type__c = 'Old Bus'` - const busRecordsQuery = await bapConnection - .sobject(BAP_BUS_TABLE) + const frf2022BusRecordsQuery = await bapConnection + .sobject("Line_Item__c") .find( { - recordtypeid: busRecordTypeId, - Related_Order_Request__c: frfRecordId, + RecordTypeId: rebateItemRecordTypeId, + Related_Order_Request__c: frf2022RecordId, CSB_Rebate_Item_Type__c: "Old Bus", }, { @@ -669,22 +741,275 @@ async function queryBapForPRFData(req, frfReviewItemId) { ) .execute(async (err, records) => ((await err) ? err : records)); - return { frfRecordQuery, busRecordsQuery }; + return { frf2022RecordQuery, frf2022BusRecordsQuery }; +} + +/** + * Uses cached JSforce connection to query the BAP for 2023 FRF submission data, + * for use in a brand new 2023 PRF submission. + * + * @param {express.Request} req + * @param {string} frfReviewItemId CSB Rebate ID with the form/version ID (9 digits) + * @returns {Promise} 2023 FRF submission fields + */ +async function queryBapFor2023PRFData(req, frfReviewItemId) { + const logMessage = + `Querying the BAP for 2023 FRF submission associated with ` + + `FRF Review Item ID: '${frfReviewItemId}'.`; + log({ level: "info", message: logMessage }); + + /** @type {jsforce.Connection} */ + const { bapConnection } = req.app.locals; + + // `SELECT + // Id + // FROM + // RecordType + // WHERE + // DeveloperName = 'CSB_Funding_Request_2023' AND + // SObjectType = 'Order_Request__c' + // LIMIT 1` + + const frf2023RecordTypeIdQuery = await bapConnection + .sobject("RecordType") + .find( + { + DeveloperName: "CSB_Funding_Request_2023", + SObjectType: "Order_Request__c", + }, + { + // "*": 1, + Id: 1, // Salesforce record ID + }, + ) + .limit(1) + .execute(async (err, records) => ((await err) ? err : records)); + + const frf2023RecordTypeId = frf2023RecordTypeIdQuery["0"].Id; + + // `SELECT + // Id, + // Primary_Applicant__r.FirstName, + // Primary_Applicant__r.LastName, + // Primary_Applicant__r.Title, + // Primary_Applicant__r.Email, + // Primary_Applicant__r.Phone, + // Alternate_Applicant__r.FirstName, + // Alternate_Applicant__r.LastName, + // Alternate_Applicant__r.Title, + // Alternate_Applicant__r.Email, + // Alternate_Applicant__r.Phone, + // CSB_School_District__r.Name, + // CSB_School_District__r.BillingStreet, + // CSB_School_District__r.BillingCity, + // CSB_School_District__r.BillingState, + // CSB_School_District__r.BillingPostalCode, + // School_District_Contact__r.FirstName, + // School_District_Contact__r.LastName, + // School_District_Contact__r.Title, + // School_District_Contact__r.Email, + // School_District_Contact__r.Phone, + // CSB_NCES_ID__c, + // School_District_Prioritized__c, + // School_District_Poverty_Rate__c, + // Prioritized_as_High_Need__c, + // Prioritized_as_Tribal__c, + // Prioritized_as_Rural__c + // FROM + // Order_Request__c + // WHERE + // RecordTypeId = '${frf2023RecordTypeId}' AND + // CSB_Review_Item_ID__c = '${frfReviewItemId}' AND + // Latest_Version__c = TRUE` + + const frf2023RecordQuery = await bapConnection + .sobject("Order_Request__c") + .find( + { + RecordTypeId: frf2023RecordTypeId, + CSB_Review_Item_ID__c: frfReviewItemId, + Latest_Version__c: true, + }, + { + // "*": 1, + Id: 1, // Salesforce record ID + "Primary_Applicant__r.FirstName": 1, + "Primary_Applicant__r.LastName": 1, + "Primary_Applicant__r.Title": 1, + "Primary_Applicant__r.Email": 1, + "Primary_Applicant__r.Phone": 1, + "Alternate_Applicant__r.FirstName": 1, + "Alternate_Applicant__r.LastName": 1, + "Alternate_Applicant__r.Title": 1, + "Alternate_Applicant__r.Email": 1, + "Alternate_Applicant__r.Phone": 1, + "CSB_School_District__r.Name": 1, + "CSB_School_District__r.BillingStreet": 1, + "CSB_School_District__r.BillingCity": 1, + "CSB_School_District__r.BillingState": 1, + "CSB_School_District__r.BillingPostalCode": 1, + "School_District_Contact__r.FirstName": 1, + "School_District_Contact__r.LastName": 1, + "School_District_Contact__r.Title": 1, + "School_District_Contact__r.Email": 1, + "School_District_Contact__r.Phone": 1, + CSB_NCES_ID__c: 1, + School_District_Prioritized__c: 1, + School_District_Poverty_Rate__c: 1, + Prioritized_as_High_Need__c: 1, + Prioritized_as_Tribal__c: 1, + Prioritized_as_Rural__c: 1, + }, + ) + .execute(async (err, records) => ((await err) ? err : records)); + + const frf2023RecordId = frf2023RecordQuery["0"].Id; + + // `SELECT + // Id + // FROM + // RecordType + // WHERE + // DeveloperName = 'CSB_Rebate_Item' AND + // SObjectType = 'Line_Item__c' + // LIMIT 1` + + const rebateItemRecordTypeIdQuery = await bapConnection + .sobject("RecordType") + .find( + { + DeveloperName: "CSB_Rebate_Item", + SObjectType: "Line_Item__c", + }, + { + // "*": 1, + Id: 1, // Salesforce record ID + }, + ) + .limit(1) + .execute(async (err, records) => ((await err) ? err : records)); + + const rebateItemRecordTypeId = rebateItemRecordTypeIdQuery["0"].Id; + + // `SELECT + // Id, + // Rebate_Item_num__c, + // CSB_VIN__c, + // CSB_Fuel_Type__c, + // CSB_GVWR__c, + // Old_Bus_Odometer_miles__c, + // CSB_Model__c, + // CSB_Model_Year__c, + // CSB_Manufacturer__c, + // CSB_Manufacturer_if_Other__c, + // CSB_Annual_Fuel_Consumption__c, + // Annual_Mileage__c, + // Old_Bus_Estimated_Remaining_Life__c, + // Old_Bus_Annual_Idling_Hours__c, + // CSB_Funds_Requested__c, + // New_Bus_Fuel_Type__c, + // New_Bus_GVWR__c, + // New_Bus_ADA_Compliant__c + // FROM + // Line_Item__c + // WHERE + // RecordTypeId = '${rebateItemRecordTypeId}' AND + // Related_Order_Request__c = '${frf2023RecordId}' AND + // CSB_Rebate_Item_Type__c = 'Old Bus'` + + const frf2023BusRecordsQuery = await bapConnection + .sobject("Line_Item__c") + .find( + { + RecordTypeId: rebateItemRecordTypeId, + Related_Order_Request__c: frf2023RecordId, + CSB_Rebate_Item_Type__c: "Old Bus", + }, + { + // "*": 1, + Id: 1, // Salesforce record ID + Rebate_Item_num__c: 1, + CSB_VIN__c: 1, + CSB_Fuel_Type__c: 1, + CSB_GVWR__c: 1, + Old_Bus_Odometer_miles__c: 1, + CSB_Model__c: 1, + CSB_Model_Year__c: 1, + CSB_Manufacturer__c: 1, + CSB_Manufacturer_if_Other__c: 1, + CSB_Annual_Fuel_Consumption__c: 1, + Annual_Mileage__c: 1, + Old_Bus_Estimated_Remaining_Life__c: 1, + Old_Bus_Annual_Idling_Hours__c: 1, + CSB_Funds_Requested__c: 1, + New_Bus_Fuel_Type__c: 1, + New_Bus_GVWR__c: 1, + New_Bus_ADA_Compliant__c: 1, + }, + ) + .execute(async (err, records) => ((await err) ? err : records)); + + const frf2023BusRecordsContactsQueries = await Promise.all( + frf2023BusRecordsQuery.map(async (frf2023BusRecord) => { + const frf2023BusRecordId = frf2023BusRecord.Id; + + // `SELECT + // Id, + // Related_Line_Item__c, + // Relationship_Type__c, + // Contact_Organization_Name__c, + // Contact__r.FirstName, + // Contact__r.LastName + // FROM + // Line_Item__c + // WHERE + // RecordTypeId = '${rebateItemRecordTypeId}' AND + // Related_Line_Item__c = '${frf2023BusRecordId}' AND + // CSB_Rebate_Item_Type__c = 'COF Relationship'` + + return await bapConnection + .sobject("Line_Item__c") + .find( + { + RecordTypeId: rebateItemRecordTypeId, + Related_Line_Item__c: frf2023BusRecordId, + CSB_Rebate_Item_Type__c: "COF Relationship", + }, + { + // "*": 1, + Id: 1, // Salesforce record ID + Related_Line_Item__c: 1, + Relationship_Type__c: 1, + Contact_Organization_Name__c: 1, + "Contact__r.FirstName": 1, + "Contact__r.LastName": 1, + }, + ) + .execute(async (err, records) => ((await err) ? err : records)); + }), + ); + + return { + frf2023RecordQuery, + frf2023BusRecordsQuery, + frf2023BusRecordsContactsQueries: frf2023BusRecordsContactsQueries.flat(), + }; } /** - * Uses cached JSforce connection to query the BAP for FRF submission data and - * PRF submission data, for use in a brand new CRF submission. + * Uses cached JSforce connection to query the BAP for 2022 FRF submission data + * and 2022 PRF submission data, for use in a brand new 2022 CRF submission. * * @param {express.Request} req * @param {string} frfReviewItemId CSB Rebate ID with the form/version ID (9 digits) * @param {string} prfReviewItemId CSB Rebate ID with the form/version ID (9 digits) - * @returns {Promise} FRF and PRF submission fields + * @returns {Promise} 2022 FRF and 2022 PRF submission fields */ -async function queryBapForCRFData(req, frfReviewItemId, prfReviewItemId) { +async function queryBapFor2022CRFData(req, frfReviewItemId, prfReviewItemId) { const logMessage = - `Querying the BAP for FRF submission associated with ` + - `FRF Review Item ID: '${frfReviewItemId}' and PRF submission associated with ` + + `Querying the BAP for 2022 FRF submission associated with ` + + `FRF Review Item ID: '${frfReviewItemId}' ` + + `and 2022 PRF submission associated with ` + `PRF Review Item ID: '${prfReviewItemId}'.`; log({ level: "info", message: logMessage }); @@ -694,18 +1019,18 @@ async function queryBapForCRFData(req, frfReviewItemId, prfReviewItemId) { // `SELECT // Id // FROM - // recordtype + // RecordType // WHERE - // developername = 'CSB_Funding_Request' AND - // sobjecttype = '${BAP_FORMS_TABLE}' + // DeveloperName = 'CSB_Funding_Request' AND + // SObjectType = 'Order_Request__c' // LIMIT 1` - const frfRecordTypeIdQuery = await bapConnection - .sobject("recordtype") + const frf2022RecordTypeIdQuery = await bapConnection + .sobject("RecordType") .find( { - developername: "CSB_Funding_Request", - sobjecttype: BAP_FORMS_TABLE, + DeveloperName: "CSB_Funding_Request", + SObjectType: "Order_Request__c", }, { // "*": 1, @@ -715,7 +1040,7 @@ async function queryBapForCRFData(req, frfReviewItemId, prfReviewItemId) { .limit(1) .execute(async (err, records) => ((await err) ? err : records)); - const frfRecordTypeId = frfRecordTypeIdQuery["0"].Id; + const frf2022RecordTypeId = frf2022RecordTypeIdQuery["0"].Id; // `SELECT // Fleet_Name__c, @@ -730,17 +1055,17 @@ async function queryBapForCRFData(req, frfReviewItemId, prfReviewItemId) { // School_District_Contact__r.FirstName, // School_District_Contact__r.LastName // FROM - // ${BAP_FORMS_TABLE} + // Order_Request__c // WHERE - // recordtypeid = '${frfRecordTypeId}' AND + // RecordTypeId = '${frf2022RecordTypeId}' AND // CSB_Review_Item_ID__c = '${frfReviewItemId}' AND // Latest_Version__c = TRUE` - const frfRecordQuery = await bapConnection - .sobject(BAP_FORMS_TABLE) + const frf2022RecordQuery = await bapConnection + .sobject("Order_Request__c") .find( { - recordtypeid: frfRecordTypeId, + RecordTypeId: frf2022RecordTypeId, CSB_Review_Item_ID__c: frfReviewItemId, Latest_Version__c: true, }, @@ -764,18 +1089,18 @@ async function queryBapForCRFData(req, frfReviewItemId, prfReviewItemId) { // `SELECT // Id // FROM - // recordtype + // RecordType // WHERE - // developername = 'CSB_Payment_Request' AND - // sobjecttype = '${BAP_FORMS_TABLE}' + // DeveloperName = 'CSB_Payment_Request' AND + // SObjectType = 'Order_Request__c' // LIMIT 1` - const prfRecordTypeIdQuery = await bapConnection - .sobject("recordtype") + const prf2022RecordTypeIdQuery = await bapConnection + .sobject("RecordType") .find( { - developername: "CSB_Payment_Request", - sobjecttype: BAP_FORMS_TABLE, + DeveloperName: "CSB_Payment_Request", + SObjectType: "Order_Request__c", }, { // "*": 1, @@ -785,7 +1110,7 @@ async function queryBapForCRFData(req, frfReviewItemId, prfReviewItemId) { .limit(1) .execute(async (err, records) => ((await err) ? err : records)); - const prfRecordTypeId = prfRecordTypeIdQuery["0"].Id; + const prf2022RecordTypeId = prf2022RecordTypeIdQuery["0"].Id; // `SELECT // Id, @@ -816,17 +1141,17 @@ async function queryBapForCRFData(req, frfReviewItemId, prfReviewItemId) { // Total_DC_Fast_Charger_Costs__c, // Total_Other_Infrastructure_Costs__c // FROM - // ${BAP_FORMS_TABLE} + // Order_Request__c // WHERE - // recordtypeid = '${prfRecordTypeId}' AND + // RecordTypeId = '${prf2022RecordTypeId}' AND // CSB_Review_Item_ID__c = '${prfReviewItemId}' AND // Latest_Version__c = TRUE` - const prfRecordQuery = await bapConnection - .sobject(BAP_FORMS_TABLE) + const prf2022RecordQuery = await bapConnection + .sobject("Order_Request__c") .find( { - recordtypeid: prfRecordTypeId, + RecordTypeId: prf2022RecordTypeId, CSB_Review_Item_ID__c: prfReviewItemId, Latest_Version__c: true, }, @@ -863,23 +1188,23 @@ async function queryBapForCRFData(req, frfReviewItemId, prfReviewItemId) { ) .execute(async (err, records) => ((await err) ? err : records)); - const prfRecordId = prfRecordQuery["0"].Id; + const prf2022RecordId = prf2022RecordQuery["0"].Id; // `SELECT // Id // FROM - // recordtype + // RecordType // WHERE - // developername = 'CSB_Rebate_Item' AND - // sobjecttype = '${BAP_BUS_TABLE}' + // DeveloperName = 'CSB_Rebate_Item' AND + // SObjectType = 'Line_Item__c' // LIMIT 1` - const busRecordTypeIdQuery = await bapConnection - .sobject("recordtype") + const rebateItemRecordTypeIdQuery = await bapConnection + .sobject("RecordType") .find( { - developername: "CSB_Rebate_Item", - sobjecttype: BAP_BUS_TABLE, + DeveloperName: "CSB_Rebate_Item", + SObjectType: "Line_Item__c", }, { // "*": 1, @@ -889,7 +1214,7 @@ async function queryBapForCRFData(req, frfReviewItemId, prfReviewItemId) { .limit(1) .execute(async (err, records) => ((await err) ? err : records)); - const busRecordTypeId = busRecordTypeIdQuery["0"].Id; + const rebateItemRecordTypeId = rebateItemRecordTypeIdQuery["0"].Id; // `SELECT // Rebate_Item_num__c, @@ -909,18 +1234,18 @@ async function queryBapForCRFData(req, frfReviewItemId, prfReviewItemId) { // New_Bus_Rebate_Amount__c, // New_Bus_Purchase_Price__c // FROM - // ${BAP_BUS_TABLE} + // Line_Item__c // WHERE - // recordtypeid = '${busRecordTypeId}' AND - // Related_Order_Request__c = '${prfRecordId}' AND + // RecordTypeId = '${rebateItemRecordTypeId}' AND + // Related_Order_Request__c = '${prf2022RecordId}' AND // CSB_Rebate_Item_Type__c = 'New Bus'` - const busRecordsQuery = await bapConnection - .sobject(BAP_BUS_TABLE) + const prf2022busRecordsQuery = await bapConnection + .sobject("Line_Item__c") .find( { - recordtypeid: busRecordTypeId, - Related_Order_Request__c: prfRecordId, + RecordTypeId: rebateItemRecordTypeId, + Related_Order_Request__c: prf2022RecordId, CSB_Rebate_Item_Type__c: "New Bus", }, { @@ -945,7 +1270,7 @@ async function queryBapForCRFData(req, frfReviewItemId, prfReviewItemId) { ) .execute(async (err, records) => ((await err) ? err : records)); - return { frfRecordQuery, prfRecordQuery, busRecordsQuery }; + return { frf2022RecordQuery, prf2022RecordQuery, prf2022busRecordsQuery }; } /** @@ -1048,31 +1373,45 @@ function getBapFormSubmissionsStatuses(req, comboKeys) { } /** - * Fetches FRF submission data associated with a FRF Review Item ID. + * Fetches 2022 FRF submission data associated with a FRF Review Item ID. + * + * @param {express.Request} req + * @param {string} frfReviewItemId + * @returns {ReturnType} + */ +function getBapDataFor2022PRF(req, frfReviewItemId) { + return verifyBapConnection(req, { + name: queryBapFor2022PRFData, + args: [req, frfReviewItemId], + }); +} + +/** + * Fetches 2023 FRF submission data associated with a FRF Review Item ID. * * @param {express.Request} req * @param {string} frfReviewItemId - * @returns {ReturnType} + * @returns {ReturnType} */ -function getBapDataForPRF(req, frfReviewItemId) { +function getBapDataFor2023PRF(req, frfReviewItemId) { return verifyBapConnection(req, { - name: queryBapForPRFData, + name: queryBapFor2023PRFData, args: [req, frfReviewItemId], }); } /** - * Fetches FRF submission data and PRF submission data associated with a FRF - * Review Item ID and a PRF Review Item ID. + * Fetches 2022 FRF submission data and 2022 PRF submission data associated with + * a FRF Review Item ID and a PRF Review Item ID. * * @param {express.Request} req * @param {string} frfReviewItemId * @param {string} prfReviewItemId - * @returns {ReturnType} + * @returns {ReturnType} */ -function getBapDataForCRF(req, frfReviewItemId, prfReviewItemId) { +function getBapDataFor2022CRF(req, frfReviewItemId, prfReviewItemId) { return verifyBapConnection(req, { - name: queryBapForCRFData, + name: queryBapFor2022CRFData, args: [req, frfReviewItemId, prfReviewItemId], }); } @@ -1126,7 +1465,8 @@ module.exports = { getBapComboKeys, getBapFormSubmissionData, getBapFormSubmissionsStatuses, - getBapDataForPRF, - getBapDataForCRF, + getBapDataFor2022PRF, + getBapDataFor2023PRF, + getBapDataFor2022CRF, checkFormSubmissionPeriodAndBapStatus, }; diff --git a/app/server/app/utilities/formio.js b/app/server/app/utilities/formio.js index 4f509c88..bac8fbf9 100644 --- a/app/server/app/utilities/formio.js +++ b/app/server/app/utilities/formio.js @@ -1,4 +1,5 @@ const express = require("express"); +const ObjectId = require("mongodb").ObjectId; // --- const { axiosFormio, @@ -6,7 +7,12 @@ const { submissionPeriodOpen, formioCSBMetadata, } = require("../config/formio"); -const { checkFormSubmissionPeriodAndBapStatus } = require("./bap"); +const { + getBapFormSubmissionsStatuses, + getBapDataFor2022PRF, + getBapDataFor2023PRF, + checkFormSubmissionPeriodAndBapStatus, +} = require("../utilities/bap"); const log = require("./logger"); /** @@ -21,6 +27,341 @@ function getComboKeyFieldName({ rebateYear }) { : ""; } +/** + * @param {Object} param + * @param {'2022' | '2023'} param.rebateYear + */ +function getRebateIdFieldName({ rebateYear }) { + return rebateYear === "2022" + ? "hidden_bap_rebate_id" + : rebateYear === "2023" + ? "_bap_rebate_id" + : ""; +} + +/** + * @param {Object} param + * @param {'2022' | '2023'} param.rebateYear + * @param {express.Request} param.req + * @param {express.Response} param.res + */ +function fetchDataForPRFSubmission({ rebateYear, req, res }) { + /** @type {{ + * email: string + * title: string + * name: string + * entity: import('./bap.js').BapSamEntity + * comboKey: ?string + * rebateId: ?string + * frfReviewItemId: ?string + * frfFormModified: ?string + * }} */ + const { + email, + title, + name, + entity, + comboKey, + rebateId, + frfReviewItemId, + frfFormModified, + } = req.body; + + const { + UNIQUE_ENTITY_ID__c, + ENTITY_EFT_INDICATOR__c, + LEGAL_BUSINESS_NAME__c, + PHYSICAL_ADDRESS_LINE_1__c, + PHYSICAL_ADDRESS_LINE_2__c, + PHYSICAL_ADDRESS_CITY__c, + PHYSICAL_ADDRESS_PROVINCE_OR_STATE__c, + PHYSICAL_ADDRESS_ZIPPOSTAL_CODE__c, + ELEC_BUS_POC_EMAIL__c, + ALT_ELEC_BUS_POC_EMAIL__c, + GOVT_BUS_POC_EMAIL__c, + ALT_GOVT_BUS_POC_EMAIL__c, + } = entity; + + if (rebateYear === "2022") { + return getBapDataFor2022PRF(req, frfReviewItemId) + .then((results) => { + const { frf2022RecordQuery, frf2022BusRecordsQuery } = results; + + const { + CSB_NCES_ID__c, + Primary_Applicant__r, + Alternate_Applicant__r, + Applicant_Organization__r, + CSB_School_District__r, + Fleet_Name__c, + School_District_Prioritized__c, + Total_Rebate_Funds_Requested__c, + Total_Infrastructure_Funds__c, + } = frf2022RecordQuery[0]; + + const busInfo = frf2022BusRecordsQuery.map((frf2022BusRecord) => { + const { + Rebate_Item_num__c, + CSB_VIN__c, + CSB_Model_Year__c, + CSB_Fuel_Type__c, + CSB_Replacement_Fuel_Type__c, + CSB_Funds_Requested__c, + } = frf2022BusRecord; + + return { + busNum: Rebate_Item_num__c, + oldBusNcesDistrictId: CSB_NCES_ID__c, + oldBusVin: CSB_VIN__c, + oldBusModelYear: CSB_Model_Year__c, + oldBusFuelType: CSB_Fuel_Type__c, + newBusFuelType: CSB_Replacement_Fuel_Type__c, + hidden_bap_max_rebate: CSB_Funds_Requested__c, + }; + }); + + /** + * NOTE: `purchaseOrders` is initialized as an empty array to fix some + * issue with the field being changed to an object when the form loads + */ + return { + data: { + bap_hidden_entity_combo_key: comboKey, + hidden_application_form_modified: frfFormModified, + hidden_current_user_email: email, + hidden_current_user_title: title, + hidden_current_user_name: name, + hidden_sam_uei: UNIQUE_ENTITY_ID__c, + hidden_sam_efti: ENTITY_EFT_INDICATOR__c || "0000", + hidden_sam_elec_bus_poc_email: ELEC_BUS_POC_EMAIL__c, + hidden_sam_alt_elec_bus_poc_email: ALT_ELEC_BUS_POC_EMAIL__c, + hidden_sam_govt_bus_poc_email: GOVT_BUS_POC_EMAIL__c, + hidden_sam_alt_govt_bus_poc_email: ALT_GOVT_BUS_POC_EMAIL__c, + hidden_bap_rebate_id: rebateId, + hidden_bap_district_id: CSB_NCES_ID__c, + hidden_bap_primary_name: Primary_Applicant__r?.Name, + hidden_bap_primary_title: Primary_Applicant__r?.Title, + hidden_bap_primary_phone_number: Primary_Applicant__r?.Phone, + hidden_bap_primary_email: Primary_Applicant__r?.Email, + hidden_bap_alternate_name: Alternate_Applicant__r?.Name || "", + hidden_bap_alternate_title: Alternate_Applicant__r?.Title || "", + hidden_bap_alternate_phone_number: Alternate_Applicant__r?.Phone || "", // prettier-ignore + hidden_bap_alternate_email: Alternate_Applicant__r?.Email || "", + hidden_bap_org_name: Applicant_Organization__r?.Name, + hidden_bap_district_name: CSB_School_District__r?.Name, + hidden_bap_fleet_name: Fleet_Name__c, + hidden_bap_prioritized: School_District_Prioritized__c, + hidden_bap_requested_funds: Total_Rebate_Funds_Requested__c, + hidden_bap_infra_max_rebate: Total_Infrastructure_Funds__c, + busInfo, + purchaseOrders: [], + }, + /** Add custom metadata to track formio submissions from wrapper. */ + metadata: { ...formioCSBMetadata }, + state: "draft", + }; + }) + .catch((error) => { + // NOTE: logged in bap verifyBapConnection + const errorStatus = 500; + const errorMessage = `Error getting data for a new 2022 Payment Request form submission from the BAP.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); + } + + if (rebateYear === "2023") { + return getBapDataFor2023PRF(req, frfReviewItemId) + .then((results) => { + const { + frf2023RecordQuery, + frf2023BusRecordsQuery, + frf2023BusRecordsContactsQueries, + } = results; + + const { + Primary_Applicant__r, + Alternate_Applicant__r, + CSB_School_District__r, + School_District_Contact__r, + CSB_NCES_ID__c, + School_District_Prioritized__c, + School_District_Poverty_Rate__c, + Prioritized_as_High_Need__c, + Prioritized_as_Tribal__c, + Prioritized_as_Rural__c, + } = frf2023RecordQuery[0]; + + // TODO: ask BAP for the query for the fields below. + // NOTE: the data from the 2023 FRF is in an 'organizations' field (array of objects) + // which has the exact same fields below, except for "_org_typeCombined" + const org_organizations = new Array(0).map((item) => ({ + org_number: Infinity, + org_type: { + existingBusOwner: true, + newBusOwner: true, + privateFleet: false, + }, + _org_typeCombined: "", // NOTE: was 'org_hidden_type' in the FRF (example value: 'Existing Bus Owner, New Bus Owner') + org_orgName: "", + org_contactFName: "", + org_contactLName: "", + org_contactTitle: "", + org_contactEmail: "", + org_contactPhone: "", + org_address1: "", + org_address2: "", + org_county: "", + org_city: "", + org_state: { + name: "", + abbreviation: "", + }, + org_zip: "", + })); + + const bus_buses = frf2023BusRecordsQuery.map((frf2023BusRecord) => { + const { + Id, + Rebate_Item_num__c, + CSB_VIN__c, + CSB_Fuel_Type__c, + CSB_GVWR__c, + Old_Bus_Odometer_miles__c, + CSB_Model__c, + CSB_Model_Year__c, + CSB_Manufacturer__c, + CSB_Manufacturer_if_Other__c, + CSB_Annual_Fuel_Consumption__c, + Annual_Mileage__c, + Old_Bus_Estimated_Remaining_Life__c, + Old_Bus_Annual_Idling_Hours__c, + CSB_Funds_Requested__c, + New_Bus_Fuel_Type__c, + New_Bus_GVWR__c, + New_Bus_ADA_Compliant__c, + } = frf2023BusRecord; + + const existingOwnerRecord = frf2023BusRecordsContactsQueries.find( + ({ Related_Line_Item__c, Relationship_Type__c }) => { + return ( + Related_Line_Item__c === Id && + Relationship_Type__c === "Existing Bus Owner" + ); + }, + ); + + const newOwnerRecord = frf2023BusRecordsContactsQueries.find( + ({ Related_Line_Item__c, Relationship_Type__c }) => { + return ( + Related_Line_Item__c === Id && + Relationship_Type__c === "New Bus Owner" + ); + }, + ); + + return { + bus_busNumber: Rebate_Item_num__c, + bus_existingOwner: { + org: "", // TODO: ask BAP how to get this value + organization: existingOwnerRecord?.Contact_Organization_Name__c, + orgContactFName: existingOwnerRecord?.Contact__r?.FirstName, + orgContactLName: existingOwnerRecord?.Contact__r?.LastName, + }, + bus_existingVin: CSB_VIN__c, + bus_existingFuelType: CSB_Fuel_Type__c, + bus_existingGvwr: CSB_GVWR__c, + bus_existingOdometer: Old_Bus_Odometer_miles__c, + bus_existingModel: CSB_Model__c, + bus_existingModelYear: CSB_Model_Year__c, + bus_existingManufacturer: CSB_Manufacturer__c, + bus_existingManufacturerOther: CSB_Manufacturer_if_Other__c, + bus_existingAnnualFuelConsumption: CSB_Annual_Fuel_Consumption__c, + bus_existingAnnualMileage: Annual_Mileage__c, + bus_existingRemainingLife: Old_Bus_Estimated_Remaining_Life__c, + bus_existingIdlingHours: Old_Bus_Annual_Idling_Hours__c, + bus_newOwner: { + org: "", // TODO: ask BAP how to get this value + organization: newOwnerRecord?.Contact_Organization_Name__c, + orgContactFName: newOwnerRecord?.Contact__r?.FirstName, + orgContactLName: newOwnerRecord?.Contact__r?.LastName, + }, + bus_rebate: CSB_Funds_Requested__c, + bus_newFuelType: New_Bus_Fuel_Type__c, + bus_newGvwr: New_Bus_GVWR__c, + _bus_newADAfromFRF: New_Bus_ADA_Compliant__c, + }; + }); + + return { + data: { + _application_form_modified: frfFormModified, + _bap_entity_combo_key: comboKey, + _bap_rebate_id: rebateId, + _user_email: email, + _user_title: title, + _user_name: name, + _bap_applicant_email: email, + _bap_applicant_title: title, + _bap_applicant_name: name, + _bap_applicant_efti: ENTITY_EFT_INDICATOR__c || "0000", + _bap_applicant_uei: UNIQUE_ENTITY_ID__c, + _bap_applicant_organization_name: LEGAL_BUSINESS_NAME__c, + _bap_applicant_street_address_1: PHYSICAL_ADDRESS_LINE_1__c, + _bap_applicant_street_address_2: PHYSICAL_ADDRESS_LINE_2__c, + _bap_applicant_county: "", // TODO: ask BAP how to get this value + _bap_applicant_city: PHYSICAL_ADDRESS_CITY__c, + _bap_applicant_state: PHYSICAL_ADDRESS_PROVINCE_OR_STATE__c, + _bap_applicant_zip: PHYSICAL_ADDRESS_ZIPPOSTAL_CODE__c, + _bap_elec_bus_poc_email: ELEC_BUS_POC_EMAIL__c, + _bap_alt_elec_bus_poc_email: ALT_ELEC_BUS_POC_EMAIL__c, + _bap_govt_bus_poc_email: GOVT_BUS_POC_EMAIL__c, + _bap_alt_govt_bus_poc_email: ALT_GOVT_BUS_POC_EMAIL__c, + _bap_primary_fname: Primary_Applicant__r?.FirstName, + _bap_primary_lname: Primary_Applicant__r?.LastName, + _bap_primary_title: Primary_Applicant__r?.Title, + _bap_primary_email: Primary_Applicant__r?.Email, + _bap_primary_phone_number: Primary_Applicant__r?.Phone, + _bap_alternate_fname: Alternate_Applicant__r?.FirstName, + _bap_alternate_lname: Alternate_Applicant__r?.LastName, + _bap_alternate_title: Alternate_Applicant__r?.Title, + _bap_alternate_email: Alternate_Applicant__r?.Email, + _bap_alternate_phone_number: Alternate_Applicant__r?.Phone, + _bap_district_ncesID: CSB_NCES_ID__c, + _bap_district_name: CSB_School_District__r?.Name, + _bap_district_address_1: CSB_School_District__r?.BillingStreet, // TODO: once BAP returns this field with a new line character, split on it for address line 1 and 2 + _bap_district_address_2: "", // TODO: see above + _bap_district_city: CSB_School_District__r?.BillingCity, + _bap_district_state: CSB_School_District__r?.BillingState, + _bap_district_zip: CSB_School_District__r?.BillingPostalCode, + _bap_district_priority: School_District_Prioritized__c, + _bap_district_selfCertify: School_District_Poverty_Rate__c, + _bap_district_priorityReason: { + highNeed: Prioritized_as_High_Need__c, + tribal: Prioritized_as_Tribal__c, + rural: Prioritized_as_Rural__c, + }, + _bap_district_contactFName: School_District_Contact__r?.FirstName, + _bap_district_contactLName: School_District_Contact__r?.LastName, + _bap_district_contactTitle: School_District_Contact__r?.Title, + _bap_district_contactEmail: School_District_Contact__r?.Email, + _bap_district_contactPhone: School_District_Contact__r?.Phone, + org_organizations, + bus_buses, + }, + /** Add custom metadata to track formio submissions from wrapper. */ + metadata: { ...formioCSBMetadata }, + state: "draft", + }; + }) + .catch((error) => { + // NOTE: logged in bap verifyBapConnection + const errorStatus = 500; + const errorMessage = `Error getting data for a new 2023 Payment Request form submission from the BAP.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); + } +} + /** * @param {Object} param * @param {'2022' | '2023'} param.rebateYear @@ -352,11 +693,388 @@ function updateFRFSubmission({ rebateYear, req, res }) { }); } +/** + * @param {Object} param + * @param {'2022' | '2023'} param.rebateYear + * @param {express.Request} param.req + * @param {express.Response} param.res + */ +function fetchPRFSubmissions({ rebateYear, req, res }) { + const { bapComboKeys } = req; + + const comboKeyFieldName = getComboKeyFieldName({ rebateYear }); + const comboKeySearchParam = `&data.${comboKeyFieldName}=`; + + const formioFormUrl = formUrl[rebateYear].prf; + + if (!formioFormUrl) { + const errorStatus = 400; + const errorMessage = `Formio form URL does not exist for ${rebateYear} PRF.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + const submissionsUrl = + `${formioFormUrl}/submission` + + `?sort=-modified` + + `&limit=1000000` + + comboKeySearchParam + + `${bapComboKeys.join(comboKeySearchParam)}`; + + axiosFormio(req) + .get(submissionsUrl) + .then((axiosRes) => axiosRes.data) + .then((submissions) => res.json(submissions)) + .catch((error) => { + // NOTE: error is logged in axiosFormio response interceptor + const errorStatus = error.response?.status || 500; + const errorMessage = `Error getting Formio ${rebateYear} Payment Request form submissions.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); +} + +/** + * @param {Object} param + * @param {'2022' | '2023'} param.rebateYear + * @param {express.Request} param.req + * @param {express.Response} param.res + */ +function createPRFSubmission({ rebateYear, req, res }) { + const { bapComboKeys, body } = req; + const { mail } = req.user; + const { comboKey } = body; + + const formioFormUrl = formUrl[rebateYear].prf; + + if (!formioFormUrl) { + const errorStatus = 400; + const errorMessage = `Formio form URL does not exist for ${rebateYear} PRF.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + if (!submissionPeriodOpen[rebateYear].prf) { + const errorStatus = 400; + const errorMessage = `${rebateYear} CSB Payment Request form enrollment period is closed.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + if (!bapComboKeys.includes(comboKey)) { + const logMessage = + `User with email '${mail}' attempted to post a new ${rebateYear} ` + + `PRF submission without a matching BAP combo key.`; + log({ level: "error", message: logMessage, req }); + + const errorStatus = 401; + const errorMessage = `Unauthorized.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + fetchDataForPRFSubmission({ rebateYear, req, res }).then((submission) => { + axiosFormio(req) + .post(`${formioFormUrl}/submission`, submission) + .then((axiosRes) => axiosRes.data) + .then((submission) => res.json(submission)) + .catch((error) => { + // NOTE: error is logged in axiosFormio response interceptor + const errorStatus = error.response?.status || 500; + const errorMessage = `Error posting Formio ${rebateYear} Payment Request form submission.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); + }); +} + +/** + * @param {Object} param + * @param {'2022' | '2023'} param.rebateYear + * @param {express.Request} param.req + * @param {express.Response} param.res + */ +function fetchPRFSubmission({ rebateYear, req, res }) { + const { bapComboKeys } = req; + const { mail } = req.user; + const { rebateId } = req.params; // CSB Rebate ID (6 digits) + + const comboKeyFieldName = getComboKeyFieldName({ rebateYear }); + const rebateIdFieldName = getRebateIdFieldName({ rebateYear }); + + const formioFormUrl = formUrl[rebateYear].prf; + + if (!formioFormUrl) { + const errorStatus = 400; + const errorMessage = `Formio form URL does not exist for ${rebateYear} PRF.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + const matchedPRFSubmissions = + `${formioFormUrl}/submission` + + `?data.${rebateIdFieldName}=${rebateId}` + + `&select=_id,data.${comboKeyFieldName}`; + + Promise.all([ + axiosFormio(req).get(matchedPRFSubmissions), + axiosFormio(req).get(formioFormUrl), + ]) + .then((axiosResponses) => axiosResponses.map((axiosRes) => axiosRes.data)) + .then(([submissions, schema]) => { + const submission = submissions[0]; + const mongoId = submission._id; + const comboKey = submission.data?.[comboKeyFieldName]; + + if (!bapComboKeys.includes(comboKey)) { + const logMessage = + `User with email '${mail}' attempted to access ${rebateYear} ` + + `PRF submission '${mongoId}' that they do not have access to.`; + log({ level: "warn", message: logMessage, req }); + + return res.json({ + userAccess: false, + formSchema: null, + submission: null, + }); + } + + /** NOTE: verifyMongoObjectId */ + if (mongoId && !ObjectId.isValid(mongoId)) { + const errorStatus = 400; + const errorMessage = `MongoDB ObjectId validation error for: '${mongoId}'.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + /** + * NOTE: We can't just use the returned submission data here because + * Formio returns the string literal 'YES' instead of a base64 encoded + * image string for signature fields when you query for all submissions + * matching on a field's value (`/submission?data.${rebateIdFieldName}=${rebateId}`). + * We need to query for a specific submission (e.g. `/submission/${mongoId}`), + * to have Formio return the correct signature field data. + */ + axiosFormio(req) + .get(`${formioFormUrl}/submission/${mongoId}`) + .then((axiosRes) => axiosRes.data) + .then((submission) => { + return res.json({ + userAccess: true, + formSchema: { url: formioFormUrl, json: schema }, + submission, + }); + }); + }) + .catch((error) => { + // NOTE: error is logged in axiosFormio response interceptor + const errorStatus = error.response?.status || 500; + const errorMessage = `Error getting Formio ${rebateYear} Payment Request form submission '${rebateId}'.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); +} + +/** + * @param {Object} param + * @param {'2022' | '2023'} param.rebateYear + * @param {express.Request} param.req + * @param {express.Response} param.res + */ +function updatePRFSubmission({ rebateYear, req, res }) { + const { bapComboKeys, body } = req; + const { mail } = req.user; + const { rebateId } = req.params; // CSB Rebate ID (6 digits) + const { mongoId, submission } = body; + + const comboKeyFieldName = getComboKeyFieldName({ rebateYear }); + const comboKey = submission.data?.[comboKeyFieldName]; + + const formioFormUrl = formUrl[rebateYear].prf; + + if (!formioFormUrl) { + const errorStatus = 400; + const errorMessage = `Formio form URL does not exist for ${rebateYear} PRF.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + checkFormSubmissionPeriodAndBapStatus({ + rebateYear, + formType: "prf", + mongoId, + comboKey, + req, + }) + .then(() => { + if (!bapComboKeys.includes(comboKey)) { + const logMessage = + `User with email '${mail}' attempted to update ${rebateYear} PRF ` + + `submission '${rebateId}' without a matching BAP combo key.`; + log({ level: "error", message: logMessage, req }); + + const errorStatus = 401; + const errorMessage = `Unauthorized.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + /** NOTE: verifyMongoObjectId */ + if (mongoId && !ObjectId.isValid(mongoId)) { + const errorStatus = 400; + const errorMessage = `MongoDB ObjectId validation error for: '${mongoId}'.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + /** Add custom metadata to track formio submissions from wrapper. */ + submission.metadata = { + ...submission.metadata, + ...formioCSBMetadata, + }; + + axiosFormio(req) + .put(`${formioFormUrl}/submission/${mongoId}`, submission) + .then((axiosRes) => axiosRes.data) + .then((submission) => res.json(submission)) + .catch((error) => { + // NOTE: error is logged in axiosFormio response interceptor + const errorStatus = error.response?.status || 500; + const errorMessage = `Error updating Formio ${rebateYear} Payment Request form submission '${rebateId}'.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); + }) + .catch((error) => { + const logMessage = + `User with email '${mail}' attempted to update ${rebateYear} PRF ` + + `submission '${rebateId}' when the CSB PRF enrollment period was closed.`; + log({ level: "error", message: logMessage, req }); + + const errorStatus = 400; + const errorMessage = `${rebateYear} CSB Payment Request form enrollment period is closed.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); +} + +/** + * @param {Object} param + * @param {'2022' | '2023'} param.rebateYear + * @param {express.Request} param.req + * @param {express.Response} param.res + */ +function deletePRFSubmission({ rebateYear, req, res }) { + const { bapComboKeys, body } = req; + const { mail } = req.user; + const { mongoId, rebateId, comboKey } = body; + + const formioFormUrl = formUrl[rebateYear].prf; + + if (!formioFormUrl) { + const errorStatus = 400; + const errorMessage = `Formio form URL does not exist for ${rebateYear} PRF.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + // verify post data includes one of user's BAP combo keys + if (!bapComboKeys.includes(comboKey)) { + const logMessage = + `User with email '${mail}' attempted to delete ${rebateYear} PRF ` + + `submission '${rebateId}' without a matching BAP combo key.`; + log({ level: "error", message: logMessage, req }); + + const errorStatus = 401; + const errorMessage = `Unauthorized.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + /** + * ensure the BAP status of the corresponding FRF submission is "Edits + * Requested" before deleting the FRF submission from Formio + */ + getBapFormSubmissionsStatuses(req, req.bapComboKeys) + .then((submissions) => { + const frf = submissions.find((submission) => { + return ( + submission.Parent_Rebate_ID__c === rebateId && + submission.Record_Type_Name__c.startsWith("CSB Funding Request") + ); + }); + + const frfNeedsEdits = + frf?.Parent_CSB_Rebate__r.CSB_Funding_Request_Status__c === + "Edits Requested"; + + if (!frfNeedsEdits) { + const errorStatus = 400; + const errorMessage = `${rebateYear} Application form submission '${mongoId}' does not need edits.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + axiosFormio(req) + .delete(`${formioFormUrl}/submission/${mongoId}`) + .then((axiosRes) => axiosRes.data) + .then((response) => { + const logMessage = `User with email '${mail}' successfully deleted ${rebateYear} PRF submission '${rebateId}'.`; + log({ level: "info", message: logMessage, req }); + + res.json(response); + }) + .catch((error) => { + // NOTE: error is logged in axiosFormio response interceptor + const errorStatus = error.response?.status || 500; + const errorMessage = `Error deleting ${rebateYear} Formio Payment Request form submission '${rebateId}'.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); + }) + .catch((error) => { + // NOTE: logged in bap verifyBapConnection + const errorStatus = 500; + const errorMessage = `Error getting form submissions statuses from the BAP.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); +} + +/** + * @param {Object} param + * @param {'2022' | '2023'} param.rebateYear + * @param {express.Request} param.req + * @param {express.Response} param.res + */ +function fetchCRFSubmissions({ rebateYear, req, res }) { + const { bapComboKeys } = req; + + const comboKeyFieldName = getComboKeyFieldName({ rebateYear }); + const comboKeySearchParam = `&data.${comboKeyFieldName}=`; + + const formioFormUrl = formUrl[rebateYear].crf; + + if (!formioFormUrl) { + const errorStatus = 400; + const errorMessage = `Formio form URL does not exist for ${rebateYear} CRF.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + const submissionsUrl = + `${formioFormUrl}/submission` + + `?sort=-modified` + + `&limit=1000000` + + comboKeySearchParam + + `${bapComboKeys.join(comboKeySearchParam)}`; + + axiosFormio(req) + .get(submissionsUrl) + .then((axiosRes) => axiosRes.data) + .then((submissions) => res.json(submissions)) + .catch((error) => { + // NOTE: error is logged in axiosFormio response interceptor + const errorStatus = error.response?.status || 500; + const errorMessage = `Error getting Formio ${rebateYear} Close Out form submissions.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); +} + module.exports = { uploadS3FileMetadata, downloadS3FileMetadata, + // fetchFRFSubmissions, createFRFSubmission, fetchFRFSubmission, updateFRFSubmission, + // + fetchPRFSubmissions, + createPRFSubmission, + fetchPRFSubmission, + updatePRFSubmission, + deletePRFSubmission, + // + fetchCRFSubmissions, };
+ {onSubmitSubmission.state === "submitted" ? ( + <> + Payment Request {rebateId} submitted + successfully. + > + ) : ( + <>Draft saved successfully.> + )} +
+ {onSubmitSubmission.state === "submitted" ? ( + <>Error submitting Payment Request form.> + ) : ( + <>Error saving draft.> + )} +
+ Draft saved successfully. +
+ Error saving draft. +
+ Error creating Payment Request{" "} + {frf.bap?.rebateId}. +
+ Please try again. +