From d1d449ab6801ad2471cff0b2b95a559b3337b3a8 Mon Sep 17 00:00:00 2001 From: Courtney Myers Date: Thu, 31 Oct 2024 14:20:56 -0400 Subject: [PATCH 1/6] Update queryForBapFormSubmissionData() to work with all future 2023 and 2024 forms in the BAP (assumes pattern of DeveloperName remains the same when querying the Salesforce RecordType for each form to get the Salesforce form record type id --- app/server/app/utilities/bap.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/server/app/utilities/bap.js b/app/server/app/utilities/bap.js index f2670a5b..000bf2c5 100644 --- a/app/server/app/utilities/bap.js +++ b/app/server/app/utilities/bap.js @@ -514,13 +514,13 @@ async function queryForBapFormSubmissionData( }, 2023: { frf: "CSB_Funding_Request_2023", - prf: null, // TODO: "CSB_Payment_Request_2023" - crf: null, // TODO: "CSB_Closeout_Request_2023" + prf: "CSB_Payment_Request_2023", + crf: "CSB_Closeout_Request_2023", }, 2024: { - frf: null, // TODO: "CSB_Funding_Request_2024" - prf: null, // TODO: "CSB_Payment_Request_2024" - crf: null, // TODO: "CSB_Closeout_Request_2024" + frf: "CSB_Funding_Request_2024", + prf: "CSB_Payment_Request_2024", + crf: "CSB_Closeout_Request_2024", }, }; From e5894a1d9935b4f26852f243f4cd8cc0dd94dd58 Mon Sep 17 00:00:00 2001 From: Courtney Myers Date: Fri, 1 Nov 2024 16:51:16 -0400 Subject: [PATCH 2/6] Update helpdesk route that fetched an existing form submission data from Formio and the BAP to support fetching Formio and the BAP independently, so the helpdesk page will support searching for a PRF or CRF submission via CSB Rebate Id before the BAP's ETL has been created for that form (queries Formio via the rebate id field directly instead of relying on getting the mongo id back from the BAP) --- app/server/app/routes/help.js | 269 +++++++++++++++++++++------------- 1 file changed, 169 insertions(+), 100 deletions(-) diff --git a/app/server/app/routes/help.js b/app/server/app/routes/help.js index ed437517..3fbb6046 100644 --- a/app/server/app/routes/help.js +++ b/app/server/app/routes/help.js @@ -10,6 +10,7 @@ const { } = require("../config/formio"); const { ensureAuthenticated, ensureHelpdesk } = require("../middleware"); const { getBapFormSubmissionData } = require("../utilities/bap"); +const { getRebateIdFieldName } = require("../utilities/formio"); /** * @typedef {'2022' | '2023' | '2024'} RebateYear @@ -32,95 +33,86 @@ const formioFormNameMap = new Map() .set("crf", "CSB Close Out"); /** - * Fetches data associated with a provided form submission from Formio. + * Fetches Formio form schema when provided a Formio form url. * - * @param {Object} param - * @param {RebateYear} param.rebateYear - * @param {FormType} param.formType - * @param {string} param.mongoId * @param {{ - * modified: string | null - * comboKey: string | null - * mongoId: string | null - * rebateId: string | null - * reviewItemId: string | null - * status: string | null - * }} param.bap - * @param {express.Request} param.req - * @param {express.Response} param.res + * formioFormUrl: string + * req: express.Request + * }} param */ -function fetchFormioSubmission({ - rebateYear, - formType, - mongoId, - bap, - req, - res, -}) { - /** NOTE: verifyMongoObjectId */ - if (!ObjectId.isValid(mongoId)) { - const errorStatus = 400; - const errorMessage = `MongoDB ObjectId validation error for: '${mongoId}'.`; - return res.status(errorStatus).json({ message: errorMessage }); - } - - const formName = formioFormNameMap.get(formType) || "CSB"; - const formioFormUrl = formUrl[rebateYear][formType]; - - if (!formioFormUrl) { - const errorStatus = 400; - const errorMessage = `Formio form URL does not exist for ${rebateYear} ${formName}.`; - return res.status(errorStatus).json({ message: errorMessage }); - } +function fetchFormioFormSchema({ formioFormUrl, req }) { + return axiosFormio(req) + .get(formioFormUrl) + .then((axiosRes) => axiosRes.data) + .then((schema) => ({ url: formioFormUrl, json: schema })) + .catch((error) => { + // NOTE: error is logged in axiosFormio response interceptor + throw error; + }); +} - return Promise.all([ - axiosFormio(req).get(`${formioFormUrl}/submission/${mongoId}`), - axiosFormio(req).get(formioFormUrl), - ]) - .then((responses) => responses.map((axiosRes) => axiosRes.data)) - .then(([formioSubmission, schema]) => { - return res.json({ - formSchema: { url: formioFormUrl, json: schema }, - formio: formioSubmission, - bap, - }); +/** + * Fetches Formio form submission data when provided a Formio submission url. + * + * @param {{ + * formioSubmissionUrl: string + * id: 'rebateId' | 'mongoId' + * req: express.Request + * }} param + */ +function fetchFormioSubmissionData({ formioSubmissionUrl, id, req }) { + /** + * NOTE: + * If the provided id is 'rebateId', the provided formSubmissionUrl includes + * the rebateId within it and we'll query Formio for all submissions that + * include the rebateId field and its value (which should only be one + * submission). In that case, the Formio query's response will be an array of + * submission objects, so we'll return the first one. + * + * Else, if the provided id is 'mongoId', the provided formSubmissionUrl + * includes the mongoId within it and we'll query Formio for the single + * submission. In that case, the Formio query's response will be a single + * submission object, so we'll return it. + */ + return axiosFormio(req) + .get(formioSubmissionUrl) + .then((axiosRes) => axiosRes.data) + .then((json) => { + const result = id === "rebateId" ? json[0] : json; + return result || null; }) .catch((error) => { // NOTE: error is logged in axiosFormio response interceptor - const errorStatus = error.response?.status || 500; - const errorMessage = `Error getting Formio ${rebateYear} ${formName} form submission '${mongoId}'.`; - return res.status(errorStatus).json({ message: errorMessage }); + throw error; }); } -// --- get an existing form's submission data from Formio -router.get("/formio/submission/:rebateYear/:formType/:id", (req, res) => { - const { rebateYear, formType, id } = req.params; - - // NOTE: included to support EPA API scan - if (id === formioExampleRebateId) { - return res.json({}); - } - - const rebateId = id.length === 6 ? id : null; - const mongoId = !rebateId ? id : null; - +/** + * Fetches BAP form submission data for a given rebate form. + * + * @param {{ + * rebateYear: RebateYear + * formType: FormType + * rebateId: string | null + * mongoId: string | null + * req: express.Request + * }} param + */ +function fetchBapSubmissionData({ + rebateYear, + formType, + rebateId, + mongoId, + req, +}) { return getBapFormSubmissionData({ rebateYear, formType, rebateId, mongoId, req, - }).then((bapSubmission) => { - /** - * NOTE: Some submissions will not be returned from the BAP (e.g., drafts or - * submissions not yet picked up by the BAP ETLs). - */ - if (!bapSubmission && !mongoId) { - const errorStatus = 400; - const errorMessage = `A valid MongoDB ObjectId must be provided for submissions not yet picked up by the BAP.`; - return res.status(errorStatus).json({ message: errorMessage }); - } + }).then((json) => { + if (!json) return null; const { UEI_EFTI_Combo_Key__c, @@ -130,7 +122,7 @@ router.get("/formio/submission/:rebateYear/:formType/:id", (req, res) => { Parent_Rebate_ID__c, Record_Type_Name__c, Parent_CSB_Rebate__r, - } = bapSubmission ?? {}; + } = json; const { CSB_Funding_Request_Status__c, @@ -139,34 +131,111 @@ router.get("/formio/submission/:rebateYear/:formType/:id", (req, res) => { Reimbursement_Needed__c, } = Parent_CSB_Rebate__r ?? {}; - /** - * NOTE: For submissions not in the BAP, each property of the bap object - * parameter will be null. - */ - return fetchFormioSubmission({ - rebateYear, - formType, - mongoId: CSB_Form_ID__c || mongoId, - bap: { - modified: CSB_Modified_Full_String__c || null, // ISO 8601 date time string - comboKey: UEI_EFTI_Combo_Key__c || null, // UEI + EFTI combo key - mongoId: CSB_Form_ID__c || null, // MongoDB Object ID - rebateId: Parent_Rebate_ID__c || null, // CSB Rebate ID (6 digits) - reviewItemId: CSB_Review_Item_ID__c || null, // CSB Rebate ID with form/version ID (9 digits) - status: - (Record_Type_Name__c?.startsWith("CSB Funding Request") - ? CSB_Funding_Request_Status__c - : Record_Type_Name__c?.startsWith("CSB Payment Request") - ? CSB_Payment_Request_Status__c - : Record_Type_Name__c?.startsWith("CSB Close Out Request") - ? CSB_Closeout_Request_Status__c - : "") || null, - reimbursementNeeded: Reimbursement_Needed__c || null, - }, + return { + modified: CSB_Modified_Full_String__c, // ISO 8601 date time string + comboKey: UEI_EFTI_Combo_Key__c, // UEI + EFTI combo key + mongoId: CSB_Form_ID__c, // MongoDB Object ID + rebateId: Parent_Rebate_ID__c, // CSB Rebate ID (6 digits) + reviewItemId: CSB_Review_Item_ID__c, // CSB Rebate ID with form/version ID (9 digits) + status: Record_Type_Name__c?.startsWith("CSB Funding Request") + ? CSB_Funding_Request_Status__c + : Record_Type_Name__c?.startsWith("CSB Payment Request") + ? CSB_Payment_Request_Status__c + : Record_Type_Name__c?.startsWith("CSB Close Out Request") + ? CSB_Closeout_Request_Status__c + : "", + reimbursementNeeded: Reimbursement_Needed__c, + }; + }); +} + +// --- get an existing form's submission data from Formio and the BAP +router.get("/formio/submission/:rebateYear/:formType/:id", async (req, res) => { + const { rebateYear, formType, id } = req.params; + + const result = { + formSchema: null, + formio: null, + bap: null, + }; + + // NOTE: included to support EPA API scan + if (id === formioExampleRebateId) { + return res.json({}); + } + + const rebateId = id.length === 6 ? id : null; + const mongoId = !rebateId ? id : 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 }); + } + + const formName = formioFormNameMap.get(formType) || "CSB"; + const formioFormUrl = formUrl[rebateYear][formType]; + + if (!formioFormUrl) { + const errorStatus = 400; + const errorMessage = `Formio form URL does not exist for ${rebateYear} ${formName} form.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + result.formSchema = await fetchFormioFormSchema({ formioFormUrl, req }); + + if (!result.formSchema) { + const errorStatus = 400; + const errorMessage = `Error getting Formio ${rebateYear} ${formName} form schema.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + const rebateIdFieldName = getRebateIdFieldName({ rebateYear }); + const formioSubmissionUrl = rebateId + ? `${formioFormUrl}/submission?data.${rebateIdFieldName}=${rebateId}` + : `${formioFormUrl}/submission/${mongoId}`; + + /** + * NOTE: FRF submissions don't include a CSB Rebate Id field, as it's created + * by the BAP after they ETL the FRF submissions. So if the user searched for + * an FRF submission with a CSB Rebate Id, we'll need to use the returned + * MongoDB ObjectId from the upcoming BAP query's response and then attempt to + * re-fetch the formio submission data using that mongoId. + */ + result.formio = + rebateId && formType === "frf" + ? null + : await fetchFormioSubmissionData({ + formioSubmissionUrl, + id: rebateId ? "rebateId" : "mongoId", + req, + }); + + result.bap = await fetchBapSubmissionData({ + rebateYear, + formType, + rebateId, + mongoId, + req, + }); + + /** NOTE: See previous note above setting of `result.formio` value */ + if (!result.formio && result.bap) { + result.formio = await fetchFormioSubmissionData({ + formioSubmissionUrl: `${formioFormUrl}/submission/${result.bap.mongoId}`, + id: "mongoId", req, - res, }); - }); + } + + if (!result.formio && !result.bap) { + const errorStatus = 400; + const errorMessage = `Error getting ${rebateYear} ${formName} form submission '${rebateId | mongoId}'.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + return res.json(result); }); // --- post an update to an existing form submission to Formio (change submission to 'draft') From 9ee3a540a62a7a06132c8293227b2f6f9cd772f4 Mon Sep 17 00:00:00 2001 From: Courtney Myers Date: Mon, 4 Nov 2024 11:58:07 -0500 Subject: [PATCH 3/6] Update server help functions fetchFormioFormSchema() and fetchFormioSubmissionData() catch blocks to return null --- app/server/app/routes/help.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/server/app/routes/help.js b/app/server/app/routes/help.js index 3fbb6046..5af9b622 100644 --- a/app/server/app/routes/help.js +++ b/app/server/app/routes/help.js @@ -45,9 +45,9 @@ function fetchFormioFormSchema({ formioFormUrl, req }) { .get(formioFormUrl) .then((axiosRes) => axiosRes.data) .then((schema) => ({ url: formioFormUrl, json: schema })) - .catch((error) => { + .catch((_error) => { // NOTE: error is logged in axiosFormio response interceptor - throw error; + return null; }); } @@ -81,9 +81,9 @@ function fetchFormioSubmissionData({ formioSubmissionUrl, id, req }) { const result = id === "rebateId" ? json[0] : json; return result || null; }) - .catch((error) => { + .catch((_error) => { // NOTE: error is logged in axiosFormio response interceptor - throw error; + return null; }); } From b06eb1658de254855187016d062f1e594c3485b2 Mon Sep 17 00:00:00 2001 From: Courtney Myers Date: Mon, 4 Nov 2024 16:23:49 -0500 Subject: [PATCH 4/6] Update server help api route that fetches a form's submission data to return the rebate id --- app/server/app/routes/help.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/server/app/routes/help.js b/app/server/app/routes/help.js index 5af9b622..6e71376e 100644 --- a/app/server/app/routes/help.js +++ b/app/server/app/routes/help.js @@ -154,6 +154,7 @@ router.get("/formio/submission/:rebateYear/:formType/:id", async (req, res) => { const { rebateYear, formType, id } = req.params; const result = { + rebateId: null, formSchema: null, formio: null, bap: null, @@ -235,7 +236,13 @@ router.get("/formio/submission/:rebateYear/:formType/:id", async (req, res) => { return res.status(errorStatus).json({ message: errorMessage }); } - return res.json(result); + const bapRebateId = result.bap?.rebateId; + const formioRebateId = result.formio?.data?.[rebateIdFieldName]; + + return res.json({ + ...result, + rebateId: bapRebateId || formioRebateId || null, + }); }); // --- post an update to an existing form submission to Formio (change submission to 'draft') From 443aff7c3ba3740e359b239692027bc0f982a419 Mon Sep 17 00:00:00 2001 From: Courtney Myers Date: Mon, 4 Nov 2024 16:28:17 -0500 Subject: [PATCH 5/6] Update helpdesk component to work with updated data returned by the server app's help API (allows better searching/display via rebate id alone) --- app/client/src/routes/helpdesk.tsx | 64 +++++++++++++++--------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/app/client/src/routes/helpdesk.tsx b/app/client/src/routes/helpdesk.tsx index 91732803..23f8080a 100644 --- a/app/client/src/routes/helpdesk.tsx +++ b/app/client/src/routes/helpdesk.tsx @@ -48,22 +48,20 @@ import { useRebateYearActions, } from "@/contexts/rebateYear"; -type Response = - | { - formSchema: null; - formio: null; - bap: BapSubmissionData; - } - | { - formSchema: { url: string; json: object }; - formio: +type Response = { + rebateId: string | null; + formSchema: { url: string; json: object } | null; + formio: + | ( | FormioFRF2022Submission | FormioPRF2022Submission | FormioCRF2022Submission | FormioFRF2023Submission - | FormioPRF2023Submission; - bap: BapSubmissionData; - }; + | FormioPRF2023Submission + ) + | null; + bap: BapSubmissionData | null; +}; type SubmissionAction = { _id: string; // MongoDB ObjectId string @@ -120,22 +118,22 @@ function ResultTableRow(props: { DraftSubmission, unknown >; - lastSearchedText: string; formType: FormType; + rebateId: string | null; formio: | FormioFRF2022Submission | FormioPRF2022Submission | FormioCRF2022Submission | FormioFRF2023Submission | FormioPRF2023Submission; - bap: BapSubmissionData; + bap: BapSubmissionData | null; }) { const { setFormDisplayed, setActionsData, submissionMutation, - lastSearchedText, formType, + rebateId, formio, bap, } = props; @@ -180,11 +178,11 @@ function ResultTableRow(props: { const date = formatDate(formio.modified); const time = formatTime(formio.modified); - const bapId = lastSearchedText.length === 6 ? bap.rebateId : bap.mongoId; + const bapInternalStatus = bap?.status || ""; + const bapReimbursementNeeded = bap?.reimbursementNeeded || false; - const bapInternalStatus = bap.status || ""; + const bapStatus = bapStatusMap[rebateYear][formType].get(bapInternalStatus); const formioStatus = formioStatusMap.get(formio.state); - const bapReimbursementNeeded = bap.reimbursementNeeded || false; const needsEdits = submissionNeedsEdits({ formio, bap }); @@ -199,9 +197,7 @@ function ResultTableRow(props: { ? "Edits Requested" : crfNeedsReimbursement ? "Reimbursement Needed" - : bapStatusMap[rebateYear][formType].get(bapInternalStatus) || - formioStatus || - ""; + : bapStatus || formioStatus || ""; const nameField = formioNameField[rebateYear][formType]; const emailField = formioEmailField[rebateYear][formType]; @@ -229,11 +225,11 @@ function ResultTableRow(props: { - {bapId || mongoId} + {rebateId || mongoId} {status} - {!bapId && status === "Submitted" && ( + {!bap && status === "Submitted" && (