diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 0a13122b..97cafcae 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -75,6 +75,7 @@ jobs: FORMIO_PROJECT_NAME: ${{ secrets.FORMIO_PROJECT_NAME }} FORMIO_API_KEY: ${{ secrets.FORMIO_API_KEY }} FORMIO_PKG_AUTH_TOKEN: ${{ secrets.FORMIO_PKG_AUTH_TOKEN }} + BAP_REST_API_VERSION: ${{ secrets.BAP_REST_API_VERSION }} BAP_CLIENT_ID: ${{ secrets.BAP_CLIENT_ID }} BAP_CLIENT_SECRET: ${{ secrets.BAP_CLIENT_SECRET }} BAP_URL: ${{ secrets.BAP_URL }} @@ -177,6 +178,7 @@ jobs: cf set-env $APP_NAME "FORMIO_BASE_URL" "$FORMIO_BASE_URL" > /dev/null cf set-env $APP_NAME "FORMIO_PROJECT_NAME" "$FORMIO_PROJECT_NAME" > /dev/null cf set-env $APP_NAME "FORMIO_API_KEY" "$FORMIO_API_KEY" > /dev/null + cf set-env $APP_NAME "BAP_REST_API_VERSION" "$BAP_REST_API_VERSION" > /dev/null cf set-env $APP_NAME "BAP_CLIENT_ID" "$BAP_CLIENT_ID" > /dev/null cf set-env $APP_NAME "BAP_CLIENT_SECRET" "$BAP_CLIENT_SECRET" > /dev/null cf set-env $APP_NAME "BAP_URL" "$BAP_URL" > /dev/null diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 6b712ac2..22c4658c 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -75,6 +75,7 @@ jobs: FORMIO_PROJECT_NAME: ${{ secrets.FORMIO_PROJECT_NAME }} FORMIO_API_KEY: ${{ secrets.FORMIO_API_KEY }} FORMIO_PKG_AUTH_TOKEN: ${{ secrets.FORMIO_PKG_AUTH_TOKEN }} + BAP_REST_API_VERSION: ${{ secrets.BAP_REST_API_VERSION }} BAP_CLIENT_ID: ${{ secrets.BAP_CLIENT_ID }} BAP_CLIENT_SECRET: ${{ secrets.BAP_CLIENT_SECRET }} BAP_URL: ${{ secrets.BAP_URL }} @@ -177,6 +178,7 @@ jobs: cf set-env $APP_NAME "FORMIO_BASE_URL" "$FORMIO_BASE_URL" > /dev/null cf set-env $APP_NAME "FORMIO_PROJECT_NAME" "$FORMIO_PROJECT_NAME" > /dev/null cf set-env $APP_NAME "FORMIO_API_KEY" "$FORMIO_API_KEY" > /dev/null + cf set-env $APP_NAME "BAP_REST_API_VERSION" "$BAP_REST_API_VERSION" > /dev/null cf set-env $APP_NAME "BAP_CLIENT_ID" "$BAP_CLIENT_ID" > /dev/null cf set-env $APP_NAME "BAP_CLIENT_SECRET" "$BAP_CLIENT_SECRET" > /dev/null cf set-env $APP_NAME "BAP_URL" "$BAP_URL" > /dev/null diff --git a/app/client/src/routes/crf2022.tsx b/app/client/src/routes/crf2022.tsx index 10a5be3a..7bd63b93 100644 --- a/app/client/src/routes/crf2022.tsx +++ b/app/client/src/routes/crf2022.tsx @@ -19,6 +19,7 @@ import { useContentData, useConfigData, useBapSamData, + useSubmissionPDFQuery, useSubmissionsQueries, useSubmissions, submissionNeedsEdits, @@ -27,7 +28,7 @@ import { entityHasDebtSubjectToOffset, getUserInfo, } from "@/utilities"; -import { Loading } from "@/components/loading"; +import { Loading, LoadingButtonIcon } from "@/components/loading"; import { Message } from "@/components/message"; import { MarkdownContent } from "@/components/markdownContent"; import { useNotificationsActions } from "@/contexts/notifications"; @@ -40,6 +41,7 @@ function useFormioSubmissionQueryAndMutation(rebateId: string | undefined) { useEffect(() => { queryClient.resetQueries({ queryKey: ["formio/2022/crf-submission"] }); + queryClient.resetQueries({ queryKey: ["formio/2022/crf-pdf"] }); }, [queryClient]); const url = `${serverUrl}/api/formio/2022/crf-submission/${rebateId}`; @@ -129,6 +131,12 @@ function CloseOutRequestForm(props: { email: string }) { const { query, mutation } = useFormioSubmissionQueryAndMutation(rebateId); const { userAccess, formSchema, submission } = query.data ?? {}; + const pdfQuery = useSubmissionPDFQuery({ + rebateYear: "2022", + formType: "crf", + mongoId: submission?._id || "", + }); + /** * 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 @@ -243,6 +251,30 @@ function CloseOutRequestForm(props: { email: string }) { + {submission?._id && ( +

+ +

+ )} + {}}>
diff --git a/app/client/src/routes/frf2022.tsx b/app/client/src/routes/frf2022.tsx index a0f9c72e..cfbaa797 100644 --- a/app/client/src/routes/frf2022.tsx +++ b/app/client/src/routes/frf2022.tsx @@ -19,6 +19,7 @@ import { useContentData, useConfigData, useBapSamData, + useSubmissionPDFQuery, useSubmissionsQueries, useSubmissions, submissionNeedsEdits, @@ -27,7 +28,7 @@ import { entityHasDebtSubjectToOffset, getUserInfo, } from "@/utilities"; -import { Loading } from "@/components/loading"; +import { Loading, LoadingButtonIcon } from "@/components/loading"; import { Message } from "@/components/message"; import { MarkdownContent } from "@/components/markdownContent"; import { useDialogActions } from "@/contexts/dialog"; @@ -41,6 +42,7 @@ function useFormioSubmissionQueryAndMutation(mongoId: string | undefined) { useEffect(() => { queryClient.resetQueries({ queryKey: ["formio/2022/frf-submission"] }); + queryClient.resetQueries({ queryKey: ["formio/2022/frf-pdf"] }); }, [queryClient]); const url = `${serverUrl}/api/formio/2022/frf-submission/${mongoId}`; @@ -142,6 +144,12 @@ function FundingRequestForm(props: { email: string }) { const { query, mutation } = useFormioSubmissionQueryAndMutation(mongoId); const { userAccess, formSchema, submission } = query.data ?? {}; + const pdfQuery = useSubmissionPDFQuery({ + rebateYear: "2022", + formType: "frf", + mongoId, + }); + /** * 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 @@ -391,6 +399,28 @@ function FundingRequestForm(props: { email: string }) { )} +

+ +

+ {}}>
diff --git a/app/client/src/routes/frf2023.tsx b/app/client/src/routes/frf2023.tsx index 9683e325..3c475b5b 100644 --- a/app/client/src/routes/frf2023.tsx +++ b/app/client/src/routes/frf2023.tsx @@ -19,6 +19,7 @@ import { useContentData, useConfigData, useBapSamData, + useSubmissionPDFQuery, useSubmissionsQueries, useSubmissions, submissionNeedsEdits, @@ -27,7 +28,7 @@ import { entityHasDebtSubjectToOffset, getUserInfo, } from "@/utilities"; -import { Loading } from "@/components/loading"; +import { Loading, LoadingButtonIcon } from "@/components/loading"; import { Message } from "@/components/message"; import { MarkdownContent } from "@/components/markdownContent"; import { useDialogActions } from "@/contexts/dialog"; @@ -41,6 +42,7 @@ function useFormioSubmissionQueryAndMutation(mongoId: string | undefined) { useEffect(() => { queryClient.resetQueries({ queryKey: ["formio/2023/frf-submission"] }); + queryClient.resetQueries({ queryKey: ["formio/2023/frf-pdf"] }); }, [queryClient]); const url = `${serverUrl}/api/formio/2023/frf-submission/${mongoId}`; @@ -128,6 +130,12 @@ function FundingRequestForm(props: { email: string }) { const { query, mutation } = useFormioSubmissionQueryAndMutation(mongoId); const { userAccess, formSchema, submission } = query.data ?? {}; + const pdfQuery = useSubmissionPDFQuery({ + rebateYear: "2023", + formType: "frf", + mongoId, + }); + /** * 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 @@ -377,6 +385,28 @@ function FundingRequestForm(props: { email: string }) { )} +

+ +

+ {}}>
diff --git a/app/client/src/routes/frf2024.tsx b/app/client/src/routes/frf2024.tsx index c34d8425..dedac67c 100644 --- a/app/client/src/routes/frf2024.tsx +++ b/app/client/src/routes/frf2024.tsx @@ -19,6 +19,7 @@ import { useContentData, useConfigData, useBapSamData, + useSubmissionPDFQuery, useSubmissionsQueries, useSubmissions, submissionNeedsEdits, @@ -27,7 +28,7 @@ import { entityHasDebtSubjectToOffset, getUserInfo, } from "@/utilities"; -import { Loading } from "@/components/loading"; +import { Loading, LoadingButtonIcon } from "@/components/loading"; import { Message } from "@/components/message"; import { MarkdownContent } from "@/components/markdownContent"; import { useDialogActions } from "@/contexts/dialog"; @@ -41,6 +42,7 @@ function useFormioSubmissionQueryAndMutation(mongoId: string | undefined) { useEffect(() => { queryClient.resetQueries({ queryKey: ["formio/2024/frf-submission"] }); + queryClient.resetQueries({ queryKey: ["formio/2024/frf-pdf"] }); }, [queryClient]); const url = `${serverUrl}/api/formio/2024/frf-submission/${mongoId}`; @@ -128,6 +130,12 @@ function FundingRequestForm(props: { email: string }) { const { query, mutation } = useFormioSubmissionQueryAndMutation(mongoId); const { userAccess, formSchema, submission } = query.data ?? {}; + const pdfQuery = useSubmissionPDFQuery({ + rebateYear: "2024", + formType: "frf", + mongoId, + }); + /** * 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 @@ -377,6 +385,28 @@ function FundingRequestForm(props: { email: string }) { )} +

+ +

+ {}}>
diff --git a/app/client/src/routes/prf2022.tsx b/app/client/src/routes/prf2022.tsx index 05921b2c..1bda0afa 100644 --- a/app/client/src/routes/prf2022.tsx +++ b/app/client/src/routes/prf2022.tsx @@ -19,6 +19,7 @@ import { useContentData, useConfigData, useBapSamData, + useSubmissionPDFQuery, useSubmissionsQueries, useSubmissions, submissionNeedsEdits, @@ -27,7 +28,7 @@ import { entityHasDebtSubjectToOffset, getUserInfo, } from "@/utilities"; -import { Loading } from "@/components/loading"; +import { Loading, LoadingButtonIcon } from "@/components/loading"; import { Message } from "@/components/message"; import { MarkdownContent } from "@/components/markdownContent"; import { useNotificationsActions } from "@/contexts/notifications"; @@ -40,6 +41,7 @@ function useFormioSubmissionQueryAndMutation(rebateId: string | undefined) { useEffect(() => { queryClient.resetQueries({ queryKey: ["formio/2022/prf-submission"] }); + queryClient.resetQueries({ queryKey: ["formio/2022/prf-pdf"] }); }, [queryClient]); const url = `${serverUrl}/api/formio/2022/prf-submission/${rebateId}`; @@ -129,6 +131,12 @@ function PaymentRequestForm(props: { email: string }) { const { query, mutation } = useFormioSubmissionQueryAndMutation(rebateId); const { userAccess, formSchema, submission } = query.data ?? {}; + const pdfQuery = useSubmissionPDFQuery({ + rebateYear: "2022", + formType: "prf", + mongoId: submission?._id || "", + }); + /** * 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 @@ -264,6 +272,30 @@ function PaymentRequestForm(props: { email: string }) { + {submission?._id && ( +

+ +

+ )} + {}}>
diff --git a/app/client/src/routes/prf2023.tsx b/app/client/src/routes/prf2023.tsx index a8a34267..1ec178ec 100644 --- a/app/client/src/routes/prf2023.tsx +++ b/app/client/src/routes/prf2023.tsx @@ -19,6 +19,7 @@ import { useContentData, useConfigData, useBapSamData, + useSubmissionPDFQuery, useSubmissionsQueries, useSubmissions, submissionNeedsEdits, @@ -27,7 +28,7 @@ import { entityHasDebtSubjectToOffset, getUserInfo, } from "@/utilities"; -import { Loading } from "@/components/loading"; +import { Loading, LoadingButtonIcon } from "@/components/loading"; import { Message } from "@/components/message"; import { MarkdownContent } from "@/components/markdownContent"; import { useNotificationsActions } from "@/contexts/notifications"; @@ -40,6 +41,7 @@ function useFormioSubmissionQueryAndMutation(rebateId: string | undefined) { useEffect(() => { queryClient.resetQueries({ queryKey: ["formio/2023/prf-submission"] }); + queryClient.resetQueries({ queryKey: ["formio/2023/prf-pdf"] }); }, [queryClient]); const url = `${serverUrl}/api/formio/2023/prf-submission/${rebateId}`; @@ -129,6 +131,12 @@ function PaymentRequestForm(props: { email: string }) { const { query, mutation } = useFormioSubmissionQueryAndMutation(rebateId); const { userAccess, formSchema, submission } = query.data ?? {}; + const pdfQuery = useSubmissionPDFQuery({ + rebateYear: "2023", + formType: "prf", + mongoId: submission?._id || "", + }); + /** * 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 @@ -262,6 +270,30 @@ function PaymentRequestForm(props: { email: string }) { + {submission?._id && ( +

+ +

+ )} + {}}>
diff --git a/app/client/src/routes/prf2024.tsx b/app/client/src/routes/prf2024.tsx index 52e9e251..1c32bea9 100644 --- a/app/client/src/routes/prf2024.tsx +++ b/app/client/src/routes/prf2024.tsx @@ -19,6 +19,7 @@ import { useContentData, useConfigData, useBapSamData, + useSubmissionPDFQuery, useSubmissionsQueries, useSubmissions, submissionNeedsEdits, @@ -27,7 +28,7 @@ import { entityHasDebtSubjectToOffset, getUserInfo, } from "@/utilities"; -import { Loading } from "@/components/loading"; +import { Loading, LoadingButtonIcon } from "@/components/loading"; import { Message } from "@/components/message"; import { MarkdownContent } from "@/components/markdownContent"; import { useNotificationsActions } from "@/contexts/notifications"; @@ -40,6 +41,7 @@ function useFormioSubmissionQueryAndMutation(rebateId: string | undefined) { useEffect(() => { queryClient.resetQueries({ queryKey: ["formio/2024/prf-submission"] }); + queryClient.resetQueries({ queryKey: ["formio/2024/prf-pdf"] }); }, [queryClient]); const url = `${serverUrl}/api/formio/2024/prf-submission/${rebateId}`; @@ -129,6 +131,12 @@ function PaymentRequestForm(props: { email: string }) { const { query, mutation } = useFormioSubmissionQueryAndMutation(rebateId); const { userAccess, formSchema, submission } = query.data ?? {}; + const pdfQuery = useSubmissionPDFQuery({ + rebateYear: "2024", + formType: "prf", + mongoId: submission?._id || "", + }); + /** * 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 @@ -262,6 +270,30 @@ function PaymentRequestForm(props: { email: string }) { + {submission?._id && ( +

+ +

+ )} + {}}>
diff --git a/app/client/src/utilities.ts b/app/client/src/utilities.ts index df4324ac..c4f348f0 100644 --- a/app/client/src/utilities.ts +++ b/app/client/src/utilities.ts @@ -10,6 +10,7 @@ import { useSearchParams } from "react-router-dom"; // --- import { type RebateYear, + type FormType, type Content, type UserData, type ConfigData, @@ -187,6 +188,32 @@ export function useBapSamData() { return queryClient.getQueryData(["bap/sam"]); } +/** Custom hook to fetch a PDF of a form submission from Formio. */ +export function useSubmissionPDFQuery(options: { + rebateYear: RebateYear; + formType: FormType; + mongoId: string | undefined; +}) { + const { rebateYear, formType, mongoId } = options; + + return useQuery({ + queryKey: [`formio/${rebateYear}/${formType}-pdf`, { id: mongoId }], + queryFn: () => { + const url = `${serverUrl}/api/formio/${rebateYear}/pdf/${formType}/${mongoId}`; + return getData(url); + }, + onSuccess: (res) => { + const link = document.createElement("a"); + link.setAttribute("href", `data:application/pdf;base64,${res}`); + link.setAttribute("download", `${mongoId}.pdf`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }, + enabled: false, + }); +} + /** Custom hook to fetch Change Request form submissions from Formio. */ export function useChangeRequestsQuery( rebateYear: Year, diff --git a/app/server/.env.example b/app/server/.env.example index 1e025a36..8ed5bd55 100644 --- a/app/server/.env.example +++ b/app/server/.env.example @@ -43,7 +43,7 @@ FORMIO_2024_CHANGE_PATH= FORMIO_BASE_URL= FORMIO_PROJECT_NAME= FORMIO_API_KEY= -FORMIO_DUPLICATES_API_KEY= +BAP_REST_API_VERSION= BAP_CLIENT_ID= BAP_CLIENT_SECRET= BAP_URL= diff --git a/app/server/app/index.js b/app/server/app/index.js index 4e941af3..469ae6ea 100644 --- a/app/server/app/index.js +++ b/app/server/app/index.js @@ -62,6 +62,12 @@ const requiredEnvironmentVariables = [ "FORMIO_BASE_URL", "FORMIO_PROJECT_NAME", "FORMIO_API_KEY", + "BAP_REST_API_VERSION", + "BAP_CLIENT_ID", + "BAP_CLIENT_SECRET", + "BAP_URL", + "BAP_USER", + "BAP_PASSWORD", "S3_PUBLIC_BUCKET", "S3_PUBLIC_REGION", ]; diff --git a/app/server/app/routes/config.js b/app/server/app/routes/config.js index 03c4408e..5064c34f 100644 --- a/app/server/app/routes/config.js +++ b/app/server/app/routes/config.js @@ -10,7 +10,7 @@ const router = express.Router(); router.use(ensureAuthenticated); // --- get CSB app specific configuration -router.get("/", (req, res) => { +router.get("/", (_req, res) => { // NOTE: fallback to current year if CSB_REBATE_YEAR is not set const date = new Date(); const year = date.getFullYear().toString(); diff --git a/app/server/app/routes/formio2022.js b/app/server/app/routes/formio2022.js index 2316b65c..0504c5cf 100644 --- a/app/server/app/routes/formio2022.js +++ b/app/server/app/routes/formio2022.js @@ -9,6 +9,8 @@ const { uploadS3FileMetadata, downloadS3FileMetadata, // + fetchSubmissionPDF, + // fetchFRFSubmissions, createFRFSubmission, fetchFRFSubmission, @@ -49,6 +51,11 @@ router.post( }, ); +// --- get a PDF of a 2022 form submission from Formio +router.get("/pdf/:formType/:mongoId", fetchBapComboKeys, (req, res) => { + fetchSubmissionPDF({ rebateYear, req, res }); +}); + // --- get user's 2022 FRF submissions from Formio router.get("/frf-submissions", fetchBapComboKeys, (req, res) => { fetchFRFSubmissions({ rebateYear, req, res }); diff --git a/app/server/app/routes/formio2023.js b/app/server/app/routes/formio2023.js index f2500134..06b2e184 100644 --- a/app/server/app/routes/formio2023.js +++ b/app/server/app/routes/formio2023.js @@ -11,6 +11,8 @@ const { uploadS3FileMetadata, downloadS3FileMetadata, // + fetchSubmissionPDF, + // fetchFRFSubmissions, createFRFSubmission, fetchFRFSubmission, @@ -61,6 +63,11 @@ router.post( }, ); +// --- get a PDF of a 2023 form submission from Formio +router.get("/pdf/:formType/:mongoId", fetchBapComboKeys, (req, res) => { + fetchSubmissionPDF({ rebateYear, req, res }); +}); + // --- get user's 2023 FRF submissions from Formio router.get("/frf-submissions", fetchBapComboKeys, (req, res) => { fetchFRFSubmissions({ rebateYear, req, res }); diff --git a/app/server/app/routes/formio2024.js b/app/server/app/routes/formio2024.js index 60a32f76..e61df562 100644 --- a/app/server/app/routes/formio2024.js +++ b/app/server/app/routes/formio2024.js @@ -11,6 +11,8 @@ const { uploadS3FileMetadata, downloadS3FileMetadata, // + fetchSubmissionPDF, + // fetchFRFSubmissions, createFRFSubmission, fetchFRFSubmission, @@ -61,6 +63,11 @@ router.post( }, ); +// --- get a PDF of a 2024 form submission from Formio +router.get("/pdf/:formType/:mongoId", fetchBapComboKeys, (req, res) => { + fetchSubmissionPDF({ rebateYear, req, res }); +}); + // --- get user's 2024 FRF submissions from Formio router.get("/frf-submissions", fetchBapComboKeys, (req, res) => { fetchFRFSubmissions({ rebateYear, req, res }); diff --git a/app/server/app/routes/status.js b/app/server/app/routes/status.js index 4f5a136e..49522570 100644 --- a/app/server/app/routes/status.js +++ b/app/server/app/routes/status.js @@ -42,7 +42,7 @@ function verifySchema({ schema, substring }) { const router = express.Router(); -router.get("/app", (req, res) => { +router.get("/app", (_req, res) => { return res.json({ status: true }); }); diff --git a/app/server/app/utilities/bap.js b/app/server/app/utilities/bap.js index 6af72079..46517e73 100644 --- a/app/server/app/utilities/bap.js +++ b/app/server/app/utilities/bap.js @@ -250,6 +250,7 @@ const { submissionPeriodOpen } = require("../config/formio"); * Primary_Applicant__r: { * attributes: { type: "Contact", url: string } * Id: string + * Record_Type_Name__c: string * FirstName: string * LastName: string * Title: string @@ -259,6 +260,7 @@ const { submissionPeriodOpen } = require("../config/formio"); * Alternate_Applicant__r: { * attributes: { type: "Contact", url: string } * Id: string + * Record_Type_Name__c: string * FirstName: string * LastName: string * Title: string @@ -277,6 +279,7 @@ const { submissionPeriodOpen } = require("../config/formio"); * School_District_Contact__r: { * attributes: { type: "Contact", url: string } * Id: string + * Record_Type_Name__c: string * FirstName: string * LastName: string * Title: string @@ -320,6 +323,7 @@ const { submissionPeriodOpen } = require("../config/formio"); * Contact__r: { * attributes: { type: "Contact", url: string } * Id: string + * Record_Type_Name__c: string * FirstName: string * LastName: string * Title: string @@ -453,6 +457,7 @@ const { submissionPeriodOpen } = require("../config/formio"); const { SERVER_URL, + BAP_REST_API_VERSION, BAP_CLIENT_ID, BAP_CLIENT_SECRET, BAP_URL, @@ -466,7 +471,7 @@ const { */ function setupConnection(req) { const bapConnection = new jsforce.Connection({ - version: "62.0", + version: BAP_REST_API_VERSION, oauth2: { clientId: BAP_CLIENT_ID, clientSecret: BAP_CLIENT_SECRET, @@ -1340,12 +1345,14 @@ async function queryBapFor2024PRFData(req, frfReviewItemId) { // Applicant_Organization__r.Id // Applicant_Organization__r.County__c // Primary_Applicant__r.Id, + // Primary_Applicant__r.Record_Type_Name__c, // Primary_Applicant__r.FirstName, // Primary_Applicant__r.LastName, // Primary_Applicant__r.Title, // Primary_Applicant__r.Email, // Primary_Applicant__r.Phone, // Alternate_Applicant__r.Id, + // Alternate_Applicant__r.Record_Type_Name__c, // Alternate_Applicant__r.FirstName, // Alternate_Applicant__r.LastName, // Alternate_Applicant__r.Title, @@ -1358,6 +1365,7 @@ async function queryBapFor2024PRFData(req, frfReviewItemId) { // CSB_School_District__r.BillingState, // CSB_School_District__r.BillingPostalCode, // School_District_Contact__r.Id, + // School_District_Contact__r.Record_Type_Name__c // School_District_Contact__r.FirstName, // School_District_Contact__r.LastName, // School_District_Contact__r.Title, @@ -1392,12 +1400,14 @@ async function queryBapFor2024PRFData(req, frfReviewItemId) { "Applicant_Organization__r.Id": 1, "Applicant_Organization__r.County__c": 1, "Primary_Applicant__r.Id": 1, + "Primary_Applicant__r.Record_Type_Name__c": 1, "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.Id": 1, + "Alternate_Applicant__r.Record_Type_Name__c": 1, "Alternate_Applicant__r.FirstName": 1, "Alternate_Applicant__r.LastName": 1, "Alternate_Applicant__r.Title": 1, @@ -1410,6 +1420,7 @@ async function queryBapFor2024PRFData(req, frfReviewItemId) { "CSB_School_District__r.BillingState": 1, "CSB_School_District__r.BillingPostalCode": 1, "School_District_Contact__r.Id": 1, + "School_District_Contact__r.Record_Type_Name__c": 1, "School_District_Contact__r.FirstName": 1, "School_District_Contact__r.LastName": 1, "School_District_Contact__r.Title": 1, @@ -1523,6 +1534,7 @@ async function queryBapFor2024PRFData(req, frfReviewItemId) { // Related_Line_Item__c, // Relationship_Type__c, // Contact__r.Id, + // Contant__r.Record_Type_Name__c, // Contact__r.FirstName, // Contact__r.LastName // Contact__r.Title, @@ -1556,6 +1568,7 @@ async function queryBapFor2024PRFData(req, frfReviewItemId) { Related_Line_Item__c: 1, Relationship_Type__c: 1, "Contact__r.Id": 1, + "Contact__r.Record_Type_Name__c": 1, "Contact__r.FirstName": 1, "Contact__r.LastName": 1, "Contact__r.Title": 1, diff --git a/app/server/app/utilities/formio.js b/app/server/app/utilities/formio.js index 3878932b..863d5cb1 100644 --- a/app/server/app/utilities/formio.js +++ b/app/server/app/utilities/formio.js @@ -3,6 +3,7 @@ const ObjectId = require("mongodb").ObjectId; // --- const { axiosFormio, + formioProjectUrl, formUrl, submissionPeriodOpen, formioCSBMetadata, @@ -535,6 +536,7 @@ function fetchDataForPRFSubmission({ rebateYear, req, res }) { const { Id: contactId, + Record_Type_Name__c, FirstName, LastName, Title, @@ -588,6 +590,7 @@ function fetchDataForPRFSubmission({ rebateYear, req, res }) { _bap_org_id: orgId, _bap_org_name: orgName, _bap_org_contact_id_frf: contactId, + _bap_org_contact_recordtype: Record_Type_Name__c, _bap_org_contact_fname: FirstName, _bap_org_contact_lname: LastName, _bap_org_contact_title: Title, @@ -648,6 +651,7 @@ function fetchDataForPRFSubmission({ rebateYear, req, res }) { org_id: existingOwnerRecord?.Contact__r?.Account?.Id, org_name: existingOwnerRecord?.Contact__r?.Account?.Name, org_contact_id: existingOwnerRecord?.Contact__r?.Id, + org_contact_recordtype: existingOwnerRecord?.Contact__r?.Record_Type_Name__c, // prettier-ignore org_contact_fname: existingOwnerRecord?.Contact__r?.FirstName, org_contact_lname: existingOwnerRecord?.Contact__r?.LastName, }, @@ -668,6 +672,7 @@ function fetchDataForPRFSubmission({ rebateYear, req, res }) { org_id: newOwnerRecord?.Contact__r?.Account?.Id, org_name: newOwnerRecord?.Contact__r?.Account?.Name, org_contact_id: newOwnerRecord?.Contact__r?.Id, + org_contact_recordtype: newOwnerRecord?.Contact__r?.Record_Type_Name__c, // prettier-ignore org_contact_fname: newOwnerRecord?.Contact__r?.FirstName, org_contact_lname: newOwnerRecord?.Contact__r?.LastName, }, @@ -704,12 +709,14 @@ function fetchDataForPRFSubmission({ rebateYear, req, res }) { _bap_govt_bus_poc_email: GOVT_BUS_POC_EMAIL__c, _bap_alt_govt_bus_poc_email: ALT_GOVT_BUS_POC_EMAIL__c, _bap_primary_id: Primary_Applicant__r?.Id, + _bap_primary_recordtype: Primary_Applicant__r?.Record_Type_Name__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: Primary_Applicant__r?.Phone, _bap_alternate_id: Alternate_Applicant__r?.Id, + _bap_alternate_recordtype: Alternate_Applicant__r?.Record_Type_Name__c, // prettier-ignore _bap_alternate_fname: Alternate_Applicant__r?.FirstName, _bap_alternate_lname: Alternate_Applicant__r?.LastName, _bap_alternate_title: Alternate_Applicant__r?.Title, @@ -731,6 +738,7 @@ function fetchDataForPRFSubmission({ rebateYear, req, res }) { }, _bap_district_self_certify: Self_Certification_Category__c, _bap_district_contact_id: School_District_Contact__r?.Id, + _bap_district_contact_recordtype: School_District_Contact__r?.Record_Type_Name__c, // prettier-ignore _bap_district_contact_fname: School_District_Contact__r?.FirstName, _bap_district_contact_lname: School_District_Contact__r?.LastName, _bap_district_contact_title: School_District_Contact__r?.Title, @@ -1083,6 +1091,101 @@ function downloadS3FileMetadata({ rebateYear, req, res }) { }); } +/** + * @param {Object} param + * @param {RebateYear} param.rebateYear + * @param {express.Request} param.req + * @param {express.Response} param.res + */ +function fetchSubmissionPDF({ rebateYear, req, res }) { + const { bapComboKeys } = req; + const { mail } = req.user; + const { formType, mongoId } = req.params; + + // NOTE: included to support EPA API scan + if (mongoId === formioExampleMongoId) { + return res.json(""); + } + + /** 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 comboKeyFieldName = getComboKeyFieldName({ rebateYear }); + + const formioFormUrl = formUrl[rebateYear][formType]; + + if (!formioFormUrl) { + const errorStatus = 400; + const errorMessage = `Formio form URL does not exist for ${rebateYear} ${formType.toUpperCase()}.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + axiosFormio(req) + .get(`${formioFormUrl}/submission/${mongoId}`) + .then((axiosRes) => axiosRes.data) + .then((submission) => { + const projectId = submission.project; + const formId = submission.form; + const comboKey = submission.data?.[comboKeyFieldName]; + + if (!bapComboKeys.includes(comboKey)) { + const logMessage = + `User with email '${mail}' attempted to download a PDF of ` + + `${rebateYear} ${formType.toUpperCase()} submission '${mongoId}' ` + + `that they do not have access to.`; + log({ level: "warn", message: logMessage, req }); + + const errorStatus = 401; + const errorMessage = `Unauthorized.`; + return res.status(errorStatus).json({ message: errorMessage }); + } + + const headers = { + "x-allow": `GET:/project/${projectId}/form/${formId}/submission/${mongoId}/download`, + "x-expire": 3600, + }; + + axiosFormio(req) + .get(`${formioProjectUrl}/token`, { headers }) + .then((axiosRes) => axiosRes.data) + .then((json) => { + const url = `${formioProjectUrl}/form/${formId}/submission/${mongoId}/download?token=${json.key}`; + + axiosFormio(req) + .get(url, { responseType: "arraybuffer" }) + .then((axiosRes) => axiosRes.data) + .then((fileData) => { + const base64String = Buffer.from(fileData).toString("base64"); + res.attachment(`${mongoId}.pdf`); + res.type("application/pdf"); + res.send(base64String); + }) + .catch((error) => { + // NOTE: error is logged in axiosFormio response interceptor + const errorStatus = error.response?.status || 500; + const errorMessage = `Error getting Formio submission PDF.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); + }) + .catch((error) => { + // NOTE: error is logged in axiosFormio response interceptor + const errorStatus = error.response?.status || 500; + const errorMessage = `Error getting Formio download token.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); + }) + .catch((error) => { + // NOTE: error is logged in axiosFormio response interceptor + const errorStatus = error.response?.status || 500; + const errorMessage = `Error getting Formio ${rebateYear} ${formType.toUpperCase()} form submission '${mongoId}'.`; + return res.status(errorStatus).json({ message: errorMessage }); + }); +} + /** * @param {Object} param * @param {RebateYear} param.rebateYear @@ -1999,7 +2102,7 @@ function updateCRFSubmission({ rebateYear, req, res }) { return res.status(errorStatus).json({ message: errorMessage }); }); }) - .catch((error) => { + .catch((_error) => { const logMessage = `User with email '${mail}' attempted to update ${rebateYear} CRF ` + `submission '${rebateId}' when the CSB CRF enrollment period was closed.`; @@ -2215,6 +2318,8 @@ module.exports = { uploadS3FileMetadata, downloadS3FileMetadata, // + fetchSubmissionPDF, + // fetchFRFSubmissions, createFRFSubmission, fetchFRFSubmission, diff --git a/docs/csb-openapi.json b/docs/csb-openapi.json index be0eff94..d0b924eb 100644 --- a/docs/csb-openapi.json +++ b/docs/csb-openapi.json @@ -373,6 +373,27 @@ } } }, + "/api/formio/2022/pdf/{formType}/{mongoId}": { + "get": { + "summary": "Download a PDF of a 2022 form submission.", + "parameters": [ + { + "$ref": "#/components/parameters/formType" + }, + { + "$ref": "#/components/parameters/mongoId" + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + } + }, "/api/formio/2022/frf-submissions": { "get": { "summary": "Get user's 2022 FRF submissions from Formio.", @@ -744,6 +765,27 @@ } } }, + "/api/formio/2023/pdf/{formType}/{mongoId}": { + "get": { + "summary": "Download a PDF of a 2023 form submission.", + "parameters": [ + { + "$ref": "#/components/parameters/formType" + }, + { + "$ref": "#/components/parameters/mongoId" + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + } + }, "/api/formio/2023/frf-submissions": { "get": { "summary": "Get user's 2023 FRF submissions from Formio.", @@ -1108,6 +1150,27 @@ } } }, + "/api/formio/2024/pdf/{formType}/{mongoId}": { + "get": { + "summary": "Download a PDF of a 2024 form submission.", + "parameters": [ + { + "$ref": "#/components/parameters/formType" + }, + { + "$ref": "#/components/parameters/mongoId" + } + ], + "responses": { + "200": { + "description": "OK" + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + } + } + } + }, "/api/formio/2024/frf-submissions": { "get": { "summary": "Get user's 2024 FRF submissions from Formio.",