+ Once the submission is back in a draft state,
+ all users with access to this submission will be
+ able to further edit it.
+
+ ),
confirmText: "Yes",
- cancelText: "Cancel",
+ dismissText: "Cancel",
confirmedAction: () => {
setFormDisplayed(false);
- pageFormioDispatch({
+ formioFormDispatch({
type: "FETCH_FORMIO_DATA_REQUEST",
});
postData(
@@ -343,13 +354,13 @@ export function Helpdesk() {
{}
)
.then((res: FormioFetchedResponse) => {
- pageFormioDispatch({
+ formioFormDispatch({
type: "FETCH_FORMIO_DATA_SUCCESS",
payload: { data: res },
});
})
.catch((err) => {
- pageFormioDispatch({
+ formioFormDispatch({
type: "FETCH_FORMIO_DATA_FAILURE",
});
});
diff --git a/app/client/src/routes/newApplicationForm.tsx b/app/client/src/routes/newApplicationForm.tsx
index 083a3be7..c061aaf4 100644
--- a/app/client/src/routes/newApplicationForm.tsx
+++ b/app/client/src/routes/newApplicationForm.tsx
@@ -81,8 +81,8 @@ export function NewApplicationForm() {
>
- {csbData.data.enrollmentClosed ? (
-
+ {!csbData.data.submissionPeriodOpen.application ? (
+
) : activeSamEntities.length <= 0 ? (
) : (
diff --git a/app/client/src/routes/paymentRequestForm.tsx b/app/client/src/routes/paymentRequestForm.tsx
index 6967064f..536ea93a 100644
--- a/app/client/src/routes/paymentRequestForm.tsx
+++ b/app/client/src/routes/paymentRequestForm.tsx
@@ -1,11 +1,17 @@
import { useMemo, useEffect, useState, useRef } from "react";
-import { useNavigate, useParams } from "react-router-dom";
+import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { Formio, Form } from "@formio/react";
import { cloneDeep, isEqual } from "lodash";
import icons from "uswds/img/sprite.svg";
// ---
-import { serverUrl, getData, postData } from "../config";
+import { serverUrl, messages, getData, postData } from "../config";
import { getUserInfo } from "../utilities";
+import {
+ submissionNeedsEdits,
+ useFetchedFormSubmissions,
+ useCombinedSubmissions,
+ useSortedRebates,
+} from "routes/allRebates";
import { Loading } from "components/loading";
import { Message } from "components/message";
import { MarkdownContent } from "components/markdownContent";
@@ -13,16 +19,17 @@ import { useContentState } from "contexts/content";
import { useUserState } from "contexts/user";
import { useCsbState } from "contexts/csb";
import { useBapState } from "contexts/bap";
+import { useFormioSubmissionsState } from "contexts/formioSubmissions";
+import {
+ FormioSubmissionData,
+ FormioFetchedResponse,
+ useFormioFormState,
+ useFormioFormDispatch,
+} from "contexts/formioForm";
import {
usePageMessageState,
usePageMessageDispatch,
} from "contexts/pageMessage";
-import {
- FormioSubmissionData,
- FormioFetchedResponse,
- usePageFormioState,
- usePageFormioDispatch,
-} from "contexts/pageFormio";
function PageMessage() {
const { displayed, type, text } = usePageMessageState();
@@ -50,23 +57,44 @@ export function PaymentRequestForm() {
function PaymentRequestFormContent({ email }: { email: string }) {
const navigate = useNavigate();
const { rebateId } = useParams<"rebateId">(); // CSB Rebate ID (6 digits)
+ const [searchParams] = useSearchParams();
const { content } = useContentState();
const { csbData } = useCsbState();
- const { samEntities } = useBapState();
- const { formio } = usePageFormioState();
+ const { samEntities, formSubmissions: bapFormSubmissions } = useBapState();
+ const {
+ applicationSubmissions: formioApplicationSubmissions,
+ paymentRequestSubmissions: formioPaymentRequestSubmissions,
+ } = useFormioSubmissionsState();
+ const { formio } = useFormioFormState();
+ const formioFormDispatch = useFormioFormDispatch();
const pageMessageDispatch = usePageMessageDispatch();
- const pageFormioDispatch = usePageFormioDispatch();
+
+ // reset formio form state since it's used across pages
+ useEffect(() => {
+ formioFormDispatch({ type: "RESET_FORMIO_DATA" });
+ }, [formioFormDispatch]);
// reset page message state since it's used across pages
useEffect(() => {
pageMessageDispatch({ type: "RESET_MESSAGE" });
}, [pageMessageDispatch]);
- // reset page formio state since it's used across pages
+ useFetchedFormSubmissions();
+
+ const combinedRebates = useCombinedSubmissions();
+ const sortedRebates = useSortedRebates(combinedRebates);
+
+ // log combined 'sortedRebates' array if 'debug' search parameter exists
useEffect(() => {
- pageFormioDispatch({ type: "RESET_FORMIO_DATA" });
- }, [pageFormioDispatch]);
+ if (searchParams.has("debug") && sortedRebates.length > 0) {
+ console.log(sortedRebates);
+ }
+ }, [searchParams, sortedRebates]);
+
+ // create ref to store when 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);
// set when form submission data is initially fetched, and then re-set each
// time a successful update of the submission data is posted to forms.gov
@@ -85,7 +113,7 @@ function PaymentRequestFormContent({ email }: { email: string }) {
useState({});
useEffect(() => {
- pageFormioDispatch({ type: "FETCH_FORMIO_DATA_REQUEST" });
+ formioFormDispatch({ type: "FETCH_FORMIO_DATA_REQUEST" });
getData(`${serverUrl}/api/formio-payment-request-submission/${rebateId}`)
.then((res: FormioFetchedResponse) => {
@@ -106,15 +134,15 @@ function PaymentRequestFormContent({ email }: { email: string }) {
return data;
});
- pageFormioDispatch({
+ formioFormDispatch({
type: "FETCH_FORMIO_DATA_SUCCESS",
payload: { data: res },
});
})
.catch((err) => {
- pageFormioDispatch({ type: "FETCH_FORMIO_DATA_FAILURE" });
+ formioFormDispatch({ type: "FETCH_FORMIO_DATA_FAILURE" });
});
- }, [rebateId, pageFormioDispatch]);
+ }, [rebateId, formioFormDispatch]);
if (formio.status === "idle") {
return null;
@@ -148,7 +176,61 @@ function PaymentRequestFormContent({ email }: { email: string }) {
return ;
}
- const formIsReadOnly = submission.state === "submitted";
+ if (
+ bapFormSubmissions.status === "idle" ||
+ bapFormSubmissions.status === "pending" ||
+ formioApplicationSubmissions.status === "idle" ||
+ formioApplicationSubmissions.status === "pending" ||
+ formioPaymentRequestSubmissions.status === "idle" ||
+ formioPaymentRequestSubmissions.status === "pending"
+ ) {
+ return ;
+ }
+
+ if (
+ bapFormSubmissions.status === "failure" ||
+ formioApplicationSubmissions.status === "failure" ||
+ formioPaymentRequestSubmissions.status === "failure"
+ ) {
+ return ;
+ }
+
+ const paymentRequestFormOpen =
+ csbData.data.submissionPeriodOpen.paymentRequest;
+
+ const rebate = sortedRebates.find((item) => item.rebateId === rebateId);
+
+ const applicationNeedsEdits = !rebate
+ ? false
+ : submissionNeedsEdits({
+ formio: rebate.application.formio,
+ bap: rebate.application.bap,
+ });
+
+ const paymentRequestNeedsEdits = !rebate
+ ? false
+ : submissionNeedsEdits({
+ formio: rebate.paymentRequest.formio,
+ bap: rebate.paymentRequest.bap,
+ });
+
+ // NOTE: If a corresponding Application form submission needs edits, a warning
+ // message is displayed that this Payment Request form submission will be
+ // deleted, and the form will be rendered read-only.
+ if (applicationNeedsEdits) {
+ pageMessageDispatch({
+ type: "DISPLAY_MESSAGE",
+ payload: {
+ type: "warning",
+ text: messages.paymentRequestFormWillBeDeleted,
+ },
+ });
+ }
+
+ const formIsReadOnly =
+ applicationNeedsEdits ||
+ ((submission.state === "submitted" || !paymentRequestFormOpen) &&
+ !paymentRequestNeedsEdits);
const entityComboKey = storedSubmissionData.bap_hidden_entity_combo_key;
const entity = samEntities.data.entities.find((entity) => {
@@ -233,6 +315,12 @@ function PaymentRequestFormContent({ email }: { email: string }) {
}) => {
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 };
if (onSubmitSubmission.state === "submitted") {
@@ -267,19 +355,8 @@ function PaymentRequestFormContent({ email }: { email: string }) {
setPendingSubmissionData({});
if (onSubmitSubmission.state === "submitted") {
- pageMessageDispatch({
- type: "DISPLAY_MESSAGE",
- payload: {
- type: "success",
- text: "Form successfully submitted.",
- },
- });
-
- setTimeout(() => {
- pageMessageDispatch({ type: "RESET_MESSAGE" });
- navigate("/");
- }, 5000);
- return;
+ const submissionSuccessMessage = `Payment Request Form ${rebateId} successfully submitted.`;
+ navigate("/", { state: { submissionSuccessMessage } });
}
if (onSubmitSubmission.state === "draft") {
@@ -297,6 +374,8 @@ function PaymentRequestFormContent({ email }: { email: string }) {
}
})
.catch((err) => {
+ formIsBeingSubmitted.current = false;
+
pageMessageDispatch({
type: "DISPLAY_MESSAGE",
payload: {
diff --git a/app/manifest-production.yml b/app/manifest-production.yml
index 5fac9799..cd3f7d09 100644
--- a/app/manifest-production.yml
+++ b/app/manifest-production.yml
@@ -4,7 +4,7 @@ applications:
routes:
- route: app-prod.app.cloud.gov/csb
- route: app.epa.gov/csb
- instances: 4
+ instances: 11
memory: 128M
disk_quota: 512MB
timeout: 180
@@ -16,3 +16,5 @@ applications:
NODE_ENV: production
LOGGER_LEVEL: INFO
OPTIMIZE_MEMORY: true
+ services:
+ - s3-pub-csb-prod
diff --git a/app/package-lock.json b/app/package-lock.json
index 14c557c3..c9c2dbd2 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -1,18 +1,18 @@
{
"name": "epa-csb",
- "version": "0.1.0",
+ "version": "2.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "epa-csb",
- "version": "0.1.0",
+ "version": "2.1.0",
"license": "CC0-1.0",
"devDependencies": {
- "concurrently": "7.2.2",
- "husky": "8.0.1",
- "lint-staged": "13.0.3",
- "prettier": "2.7.1"
+ "concurrently": "7.6.0",
+ "husky": "8.0.2",
+ "lint-staged": "13.0.4",
+ "prettier": "2.8.0"
}
},
"node_modules/aggregate-error": {
@@ -239,22 +239,22 @@
"dev": true
},
"node_modules/commander": {
- "version": "9.3.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz",
- "integrity": "sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==",
+ "version": "9.4.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz",
+ "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==",
"dev": true,
"engines": {
"node": "^12.20.0 || >=14"
}
},
"node_modules/concurrently": {
- "version": "7.2.2",
- "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.2.2.tgz",
- "integrity": "sha512-DcQkI0ruil5BA/g7Xy3EWySGrFJovF5RYAYxwGvv9Jf9q9B1v3jPFP2tl6axExNf1qgF30kjoNYrangZ0ey4Aw==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz",
+ "integrity": "sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==",
"dev": true,
"dependencies": {
"chalk": "^4.1.0",
- "date-fns": "^2.16.1",
+ "date-fns": "^2.29.1",
"lodash": "^4.17.21",
"rxjs": "^7.0.0",
"shell-quote": "^1.7.3",
@@ -264,10 +264,14 @@
"yargs": "^17.3.1"
},
"bin": {
+ "conc": "dist/bin/concurrently.js",
"concurrently": "dist/bin/concurrently.js"
},
"engines": {
"node": "^12.20.0 || ^14.13.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/cross-spawn": {
@@ -285,9 +289,9 @@
}
},
"node_modules/date-fns": {
- "version": "2.28.0",
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
- "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
+ "version": "2.29.3",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
+ "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
"dev": true,
"engines": {
"node": ">=0.11"
@@ -410,9 +414,9 @@
}
},
"node_modules/husky": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz",
- "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==",
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.2.tgz",
+ "integrity": "sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==",
"dev": true,
"bin": {
"husky": "lib/bin.js"
@@ -470,33 +474,33 @@
"dev": true
},
"node_modules/lilconfig": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz",
- "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==",
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz",
+ "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/lint-staged": {
- "version": "13.0.3",
- "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.3.tgz",
- "integrity": "sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==",
+ "version": "13.0.4",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.4.tgz",
+ "integrity": "sha512-HxlHCXoYRsq9QCby5wFozmZW00hMs/9e3l+/dz6Qr8Kle4UH0kJTdABAbqhzG+3pcG6QjL9kz7NgGBfph+a5dw==",
"dev": true,
"dependencies": {
"cli-truncate": "^3.1.0",
- "colorette": "^2.0.17",
- "commander": "^9.3.0",
+ "colorette": "^2.0.19",
+ "commander": "^9.4.1",
"debug": "^4.3.4",
"execa": "^6.1.0",
- "lilconfig": "2.0.5",
- "listr2": "^4.0.5",
+ "lilconfig": "2.0.6",
+ "listr2": "^5.0.5",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"object-inspect": "^1.12.2",
"pidtree": "^0.6.0",
"string-argv": "^0.3.1",
- "yaml": "^2.1.1"
+ "yaml": "^2.1.3"
},
"bin": {
"lint-staged": "bin/lint-staged.js"
@@ -509,22 +513,22 @@
}
},
"node_modules/listr2": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz",
- "integrity": "sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==",
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-5.0.6.tgz",
+ "integrity": "sha512-u60KxKBy1BR2uLJNTWNptzWQ1ob/gjMzIJPZffAENzpZqbMZ/5PrXXOomDcevIS/+IB7s1mmCEtSlT2qHWMqag==",
"dev": true,
"dependencies": {
"cli-truncate": "^2.1.0",
- "colorette": "^2.0.16",
+ "colorette": "^2.0.19",
"log-update": "^4.0.0",
"p-map": "^4.0.0",
"rfdc": "^1.3.0",
- "rxjs": "^7.5.5",
+ "rxjs": "^7.5.7",
"through": "^2.3.8",
"wrap-ansi": "^7.0.0"
},
"engines": {
- "node": ">=12"
+ "node": "^14.13.1 || >=16.0.0"
},
"peerDependencies": {
"enquirer": ">= 2.3.0 < 3"
@@ -766,9 +770,9 @@
}
},
"node_modules/prettier": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
- "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz",
+ "integrity": "sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
@@ -833,9 +837,9 @@
"dev": true
},
"node_modules/rxjs": {
- "version": "7.5.6",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz",
- "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==",
+ "version": "7.5.7",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz",
+ "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==",
"dev": true,
"dependencies": {
"tslib": "^2.1.0"
@@ -1069,9 +1073,9 @@
}
},
"node_modules/yaml": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz",
- "integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
+ "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==",
"dev": true,
"engines": {
"node": ">= 14"
@@ -1268,19 +1272,19 @@
"dev": true
},
"commander": {
- "version": "9.3.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz",
- "integrity": "sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==",
+ "version": "9.4.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz",
+ "integrity": "sha512-5EEkTNyHNGFPD2H+c/dXXfQZYa/scCKasxWcXJaWnNJ99pnQN9Vnmqow+p+PlFPE63Q6mThaZws1T+HxfpgtPw==",
"dev": true
},
"concurrently": {
- "version": "7.2.2",
- "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.2.2.tgz",
- "integrity": "sha512-DcQkI0ruil5BA/g7Xy3EWySGrFJovF5RYAYxwGvv9Jf9q9B1v3jPFP2tl6axExNf1qgF30kjoNYrangZ0ey4Aw==",
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.6.0.tgz",
+ "integrity": "sha512-BKtRgvcJGeZ4XttiDiNcFiRlxoAeZOseqUvyYRUp/Vtd+9p1ULmeoSqGsDA+2ivdeDFpqrJvGvmI+StKfKl5hw==",
"dev": true,
"requires": {
"chalk": "^4.1.0",
- "date-fns": "^2.16.1",
+ "date-fns": "^2.29.1",
"lodash": "^4.17.21",
"rxjs": "^7.0.0",
"shell-quote": "^1.7.3",
@@ -1302,9 +1306,9 @@
}
},
"date-fns": {
- "version": "2.28.0",
- "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
- "integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
+ "version": "2.29.3",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
+ "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
"dev": true
},
"debug": {
@@ -1385,9 +1389,9 @@
"dev": true
},
"husky": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.1.tgz",
- "integrity": "sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw==",
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.2.tgz",
+ "integrity": "sha512-Tkv80jtvbnkK3mYWxPZePGFpQ/tT3HNSs/sasF9P2YfkMezDl3ON37YN6jUUI4eTg5LcyVynlb6r4eyvOmspvg==",
"dev": true
},
"indent-string": {
@@ -1421,44 +1425,44 @@
"dev": true
},
"lilconfig": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.5.tgz",
- "integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==",
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz",
+ "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==",
"dev": true
},
"lint-staged": {
- "version": "13.0.3",
- "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.3.tgz",
- "integrity": "sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug==",
+ "version": "13.0.4",
+ "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-13.0.4.tgz",
+ "integrity": "sha512-HxlHCXoYRsq9QCby5wFozmZW00hMs/9e3l+/dz6Qr8Kle4UH0kJTdABAbqhzG+3pcG6QjL9kz7NgGBfph+a5dw==",
"dev": true,
"requires": {
"cli-truncate": "^3.1.0",
- "colorette": "^2.0.17",
- "commander": "^9.3.0",
+ "colorette": "^2.0.19",
+ "commander": "^9.4.1",
"debug": "^4.3.4",
"execa": "^6.1.0",
- "lilconfig": "2.0.5",
- "listr2": "^4.0.5",
+ "lilconfig": "2.0.6",
+ "listr2": "^5.0.5",
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"object-inspect": "^1.12.2",
"pidtree": "^0.6.0",
"string-argv": "^0.3.1",
- "yaml": "^2.1.1"
+ "yaml": "^2.1.3"
}
},
"listr2": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.5.tgz",
- "integrity": "sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA==",
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/listr2/-/listr2-5.0.6.tgz",
+ "integrity": "sha512-u60KxKBy1BR2uLJNTWNptzWQ1ob/gjMzIJPZffAENzpZqbMZ/5PrXXOomDcevIS/+IB7s1mmCEtSlT2qHWMqag==",
"dev": true,
"requires": {
"cli-truncate": "^2.1.0",
- "colorette": "^2.0.16",
+ "colorette": "^2.0.19",
"log-update": "^4.0.0",
"p-map": "^4.0.0",
"rfdc": "^1.3.0",
- "rxjs": "^7.5.5",
+ "rxjs": "^7.5.7",
"through": "^2.3.8",
"wrap-ansi": "^7.0.0"
},
@@ -1622,9 +1626,9 @@
"dev": true
},
"prettier": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
- "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz",
+ "integrity": "sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==",
"dev": true
},
"require-directory": {
@@ -1667,9 +1671,9 @@
"dev": true
},
"rxjs": {
- "version": "7.5.6",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.6.tgz",
- "integrity": "sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw==",
+ "version": "7.5.7",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz",
+ "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
@@ -1833,9 +1837,9 @@
"dev": true
},
"yaml": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz",
- "integrity": "sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.1.3.tgz",
+ "integrity": "sha512-AacA8nRULjKMX2DvWvOAdBZMOfQlypSFkjcOcu9FalllIDJ1kvlREzcdIZmidQUqqeMv7jorHjq2HlLv/+c2lg==",
"dev": true
},
"yargs": {
diff --git a/app/package.json b/app/package.json
index d8d448cd..732336ba 100644
--- a/app/package.json
+++ b/app/package.json
@@ -1,6 +1,6 @@
{
"name": "epa-csb",
- "version": "2.0.0",
+ "version": "2.1.0",
"description": "U.S. EPA Clean School Bus data collection system",
"license": "CC0-1.0",
"author": "USEPA (https://www.epa.gov)",
@@ -10,10 +10,10 @@
"Devin Galloway "
],
"devDependencies": {
- "concurrently": "7.2.2",
- "husky": "8.0.1",
- "lint-staged": "13.0.3",
- "prettier": "2.7.1"
+ "concurrently": "7.6.0",
+ "husky": "8.0.2",
+ "lint-staged": "13.0.4",
+ "prettier": "2.8.0"
},
"scripts": {
"client": "cd client && npm start",
diff --git a/app/server/.env.example b/app/server/.env.example
index 2e763967..66a76026 100644
--- a/app/server/.env.example
+++ b/app/server/.env.example
@@ -10,7 +10,9 @@ SAML_IDP_CERT=
SAML_PUBLIC_KEY=publickey
JWT_PRIVATE_KEY=secret
JWT_PUBLIC_KEY=secret
-CSB_ENROLLMENT_PERIOD=closed
+CSB_APPLICATION_FORM_OPEN=true
+CSB_PAYMENT_REQUEST_FORM_OPEN=true
+CSB_CLOSE_OUT_FORM_OPEN=true
FORMIO_BASE_URL=
FORMIO_PROJECT_NAME=
FORMIO_APPLICATION_FORM_PATH=
diff --git a/app/server/app/config/formio.js b/app/server/app/config/formio.js
index d527a8fe..ec358f53 100644
--- a/app/server/app/config/formio.js
+++ b/app/server/app/config/formio.js
@@ -1,4 +1,4 @@
-const axios = require("axios").default;
+const axios = require("axios").default || require("axios"); // TODO: https://github.com/axios/axios/issues/5011
// ---
const log = require("../utilities/logger");
@@ -56,9 +56,10 @@ function axiosFormio(req) {
log({
level: "error",
- message: `Formio Error: ${status} ${config.method.toUpperCase()} ${
- config.url
- }`,
+ message:
+ `Formio Error: ${status} ` +
+ `${config.method.toUpperCase()} ${config.url}. ` +
+ `Response: ${JSON.stringify(error.response.data)}`,
req: config,
});
diff --git a/app/server/app/index.js b/app/server/app/index.js
index 39f5c013..68e80591 100644
--- a/app/server/app/index.js
+++ b/app/server/app/index.js
@@ -27,7 +27,9 @@ const requiredEnvVars = [
"SAML_PUBLIC_KEY",
"JWT_PRIVATE_KEY",
"JWT_PUBLIC_KEY",
- "CSB_ENROLLMENT_PERIOD",
+ "CSB_APPLICATION_FORM_OPEN",
+ "CSB_PAYMENT_REQUEST_FORM_OPEN",
+ "CSB_CLOSE_OUT_FORM_OPEN",
"FORMIO_BASE_URL",
"FORMIO_PROJECT_NAME",
"FORMIO_APPLICATION_FORM_PATH",
diff --git a/app/server/app/routes/api.js b/app/server/app/routes/api.js
index 7afaf5ff..679846f5 100644
--- a/app/server/app/routes/api.js
+++ b/app/server/app/routes/api.js
@@ -1,7 +1,7 @@
const { resolve } = require("node:path");
const { readFile } = require("node:fs/promises");
const express = require("express");
-const axios = require("axios").default;
+const axios = require("axios").default || require("axios"); // TODO: https://github.com/axios/axios/issues/5011
const ObjectId = require("mongodb").ObjectId;
// ---
const {
@@ -19,64 +19,62 @@ const {
} = require("../middleware");
const {
getSamEntities,
- getApplicationSubmissionsStatuses,
- getApplicationSubmission,
+ getBapFormSubmissionsStatuses,
+ getBapApplicationSubmission,
} = require("../utilities/bap");
const log = require("../utilities/logger");
-const enrollmentClosed = process.env.CSB_ENROLLMENT_PERIOD !== "open";
+const applicationFormOpen = process.env.CSB_APPLICATION_FORM_OPEN === "true";
+const paymentRequestFormOpen =
+ process.env.CSB_PAYMENT_REQUEST_FORM_OPEN === "true";
+const closeOutFormOpen = process.env.CSB_CLOSE_OUT_FORM_OPEN === "true";
const applicationFormApiPath = `${formioProjectUrl}/${formioApplicationFormPath}`;
const paymentRequestFormApiPath = `${formioProjectUrl}/${formioPaymentRequestFormPath}`;
/**
- * Returns a resolved or rejected promise, depending on if the enrollment period
- * is closed (as set via the `CSB_ENROLLMENT_PERIOD` environment variable), and
- * if the form submission has the status of "Edits Requested" or not (as stored
- * in and returned from the BAP).
+ * Returns a resolved or rejected promise, depending on if the given form's
+ * submission period is open (as set via environment variables), and if the form
+ * submission has the status of "Edits Requested" or not (as stored in and
+ * returned from the BAP).
* @param {Object} param
* @param {'application'|'payment-request'|'close-out'} param.formType
* @param {string} param.mongoId
* @param {string} param.comboKey
* @param {express.Request} param.req
*/
-function checkEnrollmentPeriodAndBapStatus({
+function checkFormSubmissionPeriodAndBapStatus({
formType,
mongoId,
comboKey,
req,
}) {
- if (formType === "application") {
- // continue if enrollment isn't closed
- if (!enrollmentClosed) {
- return Promise.resolve();
- }
-
- // else enrollment is closed, so only continue if edits are requested
- return getApplicationSubmissionsStatuses(req, [comboKey]).then(
- (submissions) => {
- const submission = submissions.find((submission) => {
- return submission.CSB_Form_ID__c === mongoId;
- });
- const status = submission?.Parent_CSB_Rebate__r?.CSB_Rebate_Status__c;
- return status === "Edits Requested"
- ? Promise.resolve()
- : Promise.reject();
- }
- );
- }
-
- if (formType === "payment-request") {
- // TODO: update when closing of payment request form's "open enrollment"
- // functionality is implemented
+ // form submission period is open, so continue
+ if (
+ (formType === "application" && applicationFormOpen) ||
+ (formType === "payment-request" && paymentRequestFormOpen) ||
+ (formType === "close-out" && closeOutFormOpen)
+ ) {
return Promise.resolve();
}
- if (formType === "close-out") {
- // TODO: update when closing of close out form's "open enrollment"
- // functionality is implemented
- return Promise.resolve();
- }
+ // form submission period is closed, so only continue if edits are requested
+ return getBapFormSubmissionsStatuses(req, [comboKey]).then((submissions) => {
+ const submission = submissions.find((s) => s.CSB_Form_ID__c === mongoId);
+
+ const statusField =
+ formType === "application"
+ ? "CSB_Funding_Request_Status__c"
+ : formType === "payment-request"
+ ? "CSB_Payment_Request_Status__c"
+ : formType === "close-out"
+ ? "CSB_Closeout_Request_Status__c"
+ : null;
+
+ return submission?.Parent_CSB_Rebate__r?.[statusField] === "Edits Requested"
+ ? Promise.resolve()
+ : Promise.reject();
+ });
}
const router = express.Router();
@@ -156,7 +154,13 @@ router.get("/helpdesk-access", ensureHelpdesk, (req, res) => {
// --- get CSB app specific data (open enrollment status, etc.)
router.get("/csb-data", (req, res) => {
- return res.json({ enrollmentClosed });
+ return res.json({
+ submissionPeriodOpen: {
+ application: applicationFormOpen,
+ paymentRequest: paymentRequestFormOpen,
+ closeOut: closeOutFormOpen,
+ },
+ });
});
// --- get user data from EPA Gateway/Login.gov
@@ -188,12 +192,12 @@ router.get("/bap-sam-data", (req, res) => {
});
});
-// --- get user's Application form submissions statuses from EPA's BAP
-router.get("/bap-application-submissions", storeBapComboKeys, (req, res) => {
- return getApplicationSubmissionsStatuses(req, req.bapComboKeys)
+// --- get user's form submissions statuses from EPA's BAP
+router.get("/bap-form-submissions", storeBapComboKeys, (req, res) => {
+ return getBapFormSubmissionsStatuses(req, req.bapComboKeys)
.then((submissions) => res.json(submissions))
.catch((error) => {
- const message = `Error getting Application form submissions statuses from BAP`;
+ const message = `Error getting form submissions statuses from BAP`;
return res.status(401).json({ message });
});
});
@@ -230,14 +234,14 @@ router.get("/formio-application-submissions", storeBapComboKeys, (req, res) => {
router.post("/formio-application-submission", storeBapComboKeys, (req, res) => {
const comboKey = req.body.data?.bap_hidden_entity_combo_key;
- if (enrollmentClosed) {
- const message = `CSB enrollment period is closed`;
+ if (!applicationFormOpen) {
+ const message = `CSB Application form enrollment period is closed`;
return res.status(400).json({ message });
}
// verify post data includes one of user's BAP combo keys
if (!req.bapComboKeys.includes(comboKey)) {
- const message = `User with email ${req.user.mail} attempted to post a new Application form without a matching BAP combo key`;
+ const message = `User with email ${req.user.mail} attempted to post a new Application form submission without a matching BAP combo key`;
log({ level: "error", message, req });
return res.status(401).json({ message: "Unauthorized" });
}
@@ -303,35 +307,36 @@ router.post(
storeBapComboKeys,
(req, res) => {
const { mongoId } = req.params;
- const comboKey = req.body.data?.bap_hidden_entity_combo_key;
+ const submission = req.body;
+ const comboKey = submission.data?.bap_hidden_entity_combo_key;
const formType = "application";
- checkEnrollmentPeriodAndBapStatus({ formType, mongoId, comboKey, req })
+ checkFormSubmissionPeriodAndBapStatus({ formType, mongoId, comboKey, req })
.then(() => {
// verify post data includes one of user's BAP combo keys
if (!req.bapComboKeys.includes(comboKey)) {
- const message = `User with email ${req.user.mail} attempted to update existing Application form without a matching BAP combo key`;
+ const message = `User with email ${req.user.mail} attempted to update Application form submission ${mongoId} without a matching BAP combo key`;
log({ level: "error", message, req });
return res.status(401).json({ message: "Unauthorized" });
}
// add custom metadata to track formio submissions from wrapper
- req.body.metadata = {
- ...req.body.metadata,
+ submission.metadata = {
+ ...submission.metadata,
...formioCsbMetadata,
};
axiosFormio(req)
- .put(`${applicationFormApiPath}/submission/${mongoId}`, req.body)
+ .put(`${applicationFormApiPath}/submission/${mongoId}`, submission)
.then((axiosRes) => axiosRes.data)
.then((submission) => res.json(submission))
.catch((error) => {
- const message = `Error updating Forms.gov Application form submission`;
+ const message = `Error updating Forms.gov Application form submission ${mongoId}`;
return res.status(error?.response?.status || 500).json({ message });
});
})
.catch((error) => {
- const message = `CSB enrollment period is closed`;
+ const message = `CSB Application form enrollment period is closed`;
return res.status(400).json({ message });
});
}
@@ -344,10 +349,10 @@ router.post(
(req, res) => {
const { formType, mongoId, comboKey } = req.params;
- checkEnrollmentPeriodAndBapStatus({ formType, mongoId, comboKey, req })
+ checkFormSubmissionPeriodAndBapStatus({ formType, mongoId, comboKey, req })
.then(() => {
if (!req.bapComboKeys.includes(comboKey)) {
- const message = `User with email ${req.user.mail} attempted to upload file without a matching BAP combo key`;
+ const message = `User with email ${req.user.mail} attempted to upload a file without a matching BAP combo key`;
log({ level: "error", message, req });
return res.status(401).json({ message: "Unauthorized" });
}
@@ -362,7 +367,15 @@ router.post(
});
})
.catch((error) => {
- const message = `CSB enrollment period is closed`;
+ const formName =
+ formType === "application"
+ ? "CSB Application"
+ ? formType === "payment-request"
+ : "CSB Payment Request"
+ ? formType === "close-out"
+ : "CSB Close-Out"
+ : "CSB";
+ const message = `${formName} form enrollment period is closed`;
return res.status(400).json({ message });
});
}
@@ -376,7 +389,7 @@ router.get(
const { comboKey } = req.params;
if (!req.bapComboKeys.includes(comboKey)) {
- const message = `User with email ${req.user.mail} attempted to download file without a matching BAP combo key`;
+ const message = `User with email ${req.user.mail} attempted to download a file without a matching BAP combo key`;
log({ level: "error", message, req });
return res.status(401).json({ message: "Unauthorized" });
}
@@ -434,7 +447,7 @@ router.post(
// verify post data includes one of user's BAP combo keys
if (!req.bapComboKeys.includes(comboKey)) {
- const message = `User with email ${req.user.mail} attempted to post a new Payment Request form without a matching BAP combo key`;
+ const message = `User with email ${req.user.mail} attempted to post a new Payment Request form submission without a matching BAP combo key`;
log({ level: "error", message, req });
return res.status(401).json({ message: "Unauthorized" });
}
@@ -448,7 +461,7 @@ router.post(
ALT_GOVT_BUS_POC_EMAIL__c,
} = entity;
- return getApplicationSubmission(req, reviewItemId)
+ return getBapApplicationSubmission(req, reviewItemId)
.then(({ formsTableRecordQuery, busTableRecordsQuery }) => {
const {
CSB_NCES_ID__c,
@@ -474,7 +487,7 @@ router.post(
// 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 newSubmission = {
+ const submission = {
data: {
bap_hidden_entity_combo_key: comboKey,
hidden_application_form_modified: applicationFormModified,
@@ -515,7 +528,7 @@ router.post(
};
axiosFormio(req)
- .post(`${paymentRequestFormApiPath}/submission`, newSubmission)
+ .post(`${paymentRequestFormApiPath}/submission`, submission)
.then((axiosRes) => axiosRes.data)
.then((submission) => res.json(submission))
.catch((error) => {
@@ -597,35 +610,100 @@ router.post(
"/formio-payment-request-submission/:rebateId",
storeBapComboKeys,
(req, res) => {
+ const { rebateId } = req.params; // CSB Rebate ID (6 digits)
const { mongoId, submission } = req.body;
const comboKey = submission.data?.bap_hidden_entity_combo_key;
+ const formType = "payment-request";
+
+ checkFormSubmissionPeriodAndBapStatus({ formType, mongoId, comboKey, req })
+ .then(() => {
+ // verify post data includes one of user's BAP combo keys
+ if (!req.bapComboKeys.includes(comboKey)) {
+ const message = `User with email ${req.user.mail} attempted to update Payment Request form submission ${rebateId} without a matching BAP combo key`;
+ log({ level: "error", message, req });
+ return res.status(401).json({ message: "Unauthorized" });
+ }
+
+ // NOTE: verifyMongoObjectId middleware content:
+ if (mongoId && !ObjectId.isValid(mongoId)) {
+ const message = `MongoDB ObjectId validation error for: ${mongoId}`;
+ return res.status(400).json({ message });
+ }
+
+ // add custom metadata to track formio submissions from wrapper
+ submission.metadata = {
+ ...submission.metadata,
+ ...formioCsbMetadata,
+ };
+
+ axiosFormio(req)
+ .put(`${paymentRequestFormApiPath}/submission/${mongoId}`, submission)
+ .then((axiosRes) => axiosRes.data)
+ .then((submission) => res.json(submission))
+ .catch((error) => {
+ const message = `Error updating Forms.gov Payment Request form submission ${rebateId}`;
+ return res.status(error?.response?.status || 500).json({ message });
+ });
+ })
+ .catch((error) => {
+ const message = `CSB Payment Request form enrollment period is closed`;
+ return res.status(400).json({ message });
+ });
+ }
+);
+
+// --- delete an existing Payment Request form submission from Forms.gov
+router.post(
+ "/delete-formio-payment-request-submission",
+ storeBapComboKeys,
+ (req, res) => {
+ const { mongoId, rebateId, comboKey } = req.body;
// verify post data includes one of user's BAP combo keys
if (!req.bapComboKeys.includes(comboKey)) {
- const message = `User with email ${req.user.mail} attempted to update existing Payment Request form without a matching BAP combo key`;
+ const message = `User with email ${req.user.mail} attempted to delete Payment Request form submission ${rebateId} without a matching BAP combo key`;
log({ level: "error", message, req });
return res.status(401).json({ message: "Unauthorized" });
}
- // NOTE: verifyMongoObjectId middleware content:
- if (mongoId && !ObjectId.isValid(mongoId)) {
- const message = `MongoDB ObjectId validation error for: ${mongoId}`;
- return res.status(400).json({ message });
- }
+ // ensure the BAP status of the corresponding Application form submission
+ // is "Edits Requested" before deleting the Payment Request form submission
+ // from Forms.gov
+ getBapFormSubmissionsStatuses(req, req.bapComboKeys)
+ .then((submissions) => {
+ const application = submissions.find((submission) => {
+ return (
+ submission.Parent_Rebate_ID__c === rebateId &&
+ submission.Record_Type_Name__c === "CSB Funding Request"
+ );
+ });
- // add custom metadata to track formio submissions from wrapper
- submission.metadata = {
- ...submission.metadata,
- ...formioCsbMetadata,
- };
+ const applicationNeedsEdits =
+ application?.Parent_CSB_Rebate__r.CSB_Funding_Request_Status__c ===
+ "Edits Requested";
- axiosFormio(req)
- .put(`${paymentRequestFormApiPath}/submission/${mongoId}`, submission)
- .then((axiosRes) => axiosRes.data)
- .then((submission) => res.json(submission))
+ if (!applicationNeedsEdits) {
+ const message = `CSB Application form submission does not need edits`;
+ return res.status(400).json({ message });
+ }
+
+ axiosFormio(req)
+ .delete(`${paymentRequestFormApiPath}/submission/${mongoId}`)
+ .then((axiosRes) => axiosRes.data)
+ .then((response) => {
+ const message = `User with email ${req.user.mail} successfully deleted Payment Request form submission ${rebateId}`;
+ log({ level: "info", message, req });
+
+ res.json(response);
+ })
+ .catch((error) => {
+ const message = `Error deleting Forms.gov Payment Request form submission ${rebateId}`;
+ return res.status(error?.response?.status || 500).json({ message });
+ });
+ })
.catch((error) => {
- const message = `Error updating Forms.gov Payment Request form submission`;
- return res.status(error?.response?.status || 500).json({ message });
+ const message = `Error getting form submissions statuses from BAP`;
+ return res.status(401).json({ message });
});
}
);
diff --git a/app/server/app/routes/help.js b/app/server/app/routes/help.js
index 8beadd4a..32372c7f 100644
--- a/app/server/app/routes/help.js
+++ b/app/server/app/routes/help.js
@@ -11,7 +11,10 @@ const {
const { ensureAuthenticated, ensureHelpdesk } = require("../middleware");
const log = require("../utilities/logger");
-const enrollmentClosed = process.env.CSB_ENROLLMENT_PERIOD !== "open";
+const applicationFormOpen = process.env.CSB_APPLICATION_FORM_OPEN === "true";
+const paymentRequestFormOpen =
+ process.env.CSB_PAYMENT_REQUEST_FORM_OPEN === "true";
+const closeOutFormOpen = process.env.CSB_CLOSE_OUT_FORM_OPEN === "true";
const applicationFormApiPath = `${formioProjectUrl}/${formioApplicationFormPath}`;
const paymentRequestFormApiPath = `${formioProjectUrl}/${formioPaymentRequestFormPath}`;
@@ -95,6 +98,10 @@ router.get("/formio-submission/:formType/:id", (req, res) => {
res.status(error?.response?.status || 500).json({ message });
});
}
+
+ if (formType === "close-out") {
+ // TODO
+ }
});
// --- change a submitted Forms.gov form's submission state back to draft
@@ -103,13 +110,13 @@ router.post("/formio-submission/:formType/:id", (req, res) => {
const { mail } = req.user;
if (formType === "application") {
- const mongoId = id;
-
- if (enrollmentClosed) {
- const message = `CSB enrollment period is closed`;
+ if (!applicationFormOpen) {
+ const message = `CSB Application form enrollment period is closed`;
return res.status(400).json({ message });
}
+ const mongoId = id;
+
// NOTE: verifyMongoObjectId middleware content:
if (mongoId && !ObjectId.isValid(mongoId)) {
const message = `MongoDB ObjectId validation error for: ${mongoId}`;
@@ -146,6 +153,11 @@ router.post("/formio-submission/:formType/:id", (req, res) => {
}
if (formType === "payment-request") {
+ if (!paymentRequestFormOpen) {
+ const message = `CSB Payment Request form enrollment period is closed`;
+ return res.status(400).json({ message });
+ }
+
const rebateId = id;
const matchedPaymentRequestFormSubmissions =
@@ -189,6 +201,15 @@ router.post("/formio-submission/:formType/:id", (req, res) => {
res.status(error?.response?.status || 500).json({ message });
});
}
+
+ if (formType === "close-out") {
+ if (!closeOutFormOpen) {
+ const message = `CSB Close-Out form enrollment period is closed`;
+ return res.status(400).json({ message });
+ }
+
+ // TODO
+ }
});
module.exports = router;
diff --git a/app/server/app/utilities/bap.js b/app/server/app/utilities/bap.js
index e5ac7b6b..e69a2338 100644
--- a/app/server/app/utilities/bap.js
+++ b/app/server/app/utilities/bap.js
@@ -37,13 +37,17 @@ const log = require("../utilities/logger");
*/
/**
- * @typedef {Object} BapApplicationSubmission
+ * @typedef {Object} BapFormSubmission
+ * @property {string} UEI_EFTI_Combo_Key__c
* @property {string} CSB_Form_ID__c
* @property {string} CSB_Modified_Full_String__c
- * @property {string} UEI_EFTI_Combo_Key__c
+ * @property {string} CSB_Review_Item_ID__c
* @property {string} Parent_Rebate_ID__c
+ * @property {string} Record_Type_Name__c
* @property {Object} Parent_CSB_Rebate__r
- * @property {string} Parent_CSB_Rebate__r.CSB_Rebate_Status__c
+ * @property {string} Parent_CSB_Rebate__r.CSB_Funding_Request_Status__c
+ * @property {string} Parent_CSB_Rebate__r.CSB_Payment_Request_Status__c
+ * @property {string} Parent_CSB_Rebate__r.CSB_Closeout_Request_Status__c
* @property {Object} attributes
* @property {string} attributes.type
* @property {string} attributes.url
@@ -178,50 +182,91 @@ async function queryForSamEntities(req, email) {
}
/**
- * Uses cached JSforce connection to query the BAP for application form submissions statuses, and related metadata.
+ * Uses cached JSforce connection to query the BAP for form submissions statuses and related metadata.
* @param {express.Request} req
* @param {string[]} comboKeys
- * @returns {Promise} collection of fields associated with each application form submission
+ * @returns {Promise} collection of fields associated with each form submission
*/
-async function queryForApplicationSubmissionsStatuses(req, comboKeys) {
- const message = `Querying BAP for Application form submissions statuses associated with combokeys: ${comboKeys}.`;
+async function queryForBapFormSubmissionsStatuses(req, comboKeys) {
+ const message = `Querying BAP for form submissions statuses associated with combokeys: ${comboKeys}.`;
log({ level: "info", message });
/** @type {jsforce.Connection} */
const bapConnection = req.app.locals.bapConnection;
+ // `SELECT
+ // Parent_Rebate_ID__c,
+ // FROM
+ // ${BAP_FORMS_TABLE}
+ // 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)
+ .find(
+ {
+ UEI_EFTI_Combo_Key__c: { $in: comboKeys },
+ Latest_Version__c: true,
+ },
+ {
+ // "*": 1,
+ Parent_Rebate_ID__c: 1, // CSB Rebate ID (6 digits)
+ }
+ )
+ .sort({ CreatedDate: -1 })
+ .execute(async (err, records) => ((await err) ? err : records));
+
+ const parentRebateIds = parentRebateIdsQuery.map((item) => {
+ return item.Parent_Rebate_ID__c;
+ });
+
// `SELECT
// UEI_EFTI_Combo_Key__c,
// CSB_Form_ID__c,
// CSB_Modified_Full_String__c,
// CSB_Review_Item_ID__c,
// Parent_Rebate_ID__c,
- // Parent_CSB_Rebate__r.CSB_Rebate_Status__c
+ // Record_Type_Name__c,
+ // Parent_CSB_Rebate__r.CSB_Funding_Request_Status__c,
+ // Parent_CSB_Rebate__r.CSB_Payment_Request_Status__c,
+ // Parent_CSB_Rebate__r.CSB_Closeout_Request_Status__c
// FROM
// ${BAP_FORMS_TABLE}
// WHERE
- // ${comboKeys
- // .map((key) => `UEI_EFTI_Combo_Key__c = '${key}'`)
- // .join(" OR ")}
+ // (${parentRebateIds
+ // .map((id) => `Parent_CSB_Rebate__r.CSB_Rebate_ID__c = '${id}'`)
+ // .join(" OR ")}) AND
+ // Latest_Version__c = TRUE
// ORDER BY
// CreatedDate DESC`
- return await bapConnection
+ const submissions = await bapConnection
.sobject(BAP_FORMS_TABLE)
.find(
- { UEI_EFTI_Combo_Key__c: { $in: comboKeys } },
+ {
+ "Parent_CSB_Rebate__r.CSB_Rebate_ID__c": { $in: parentRebateIds },
+ Latest_Version__c: true,
+ },
{
// "*": 1,
UEI_EFTI_Combo_Key__c: 1,
CSB_Form_ID__c: 1, // MongoDB ObjectId string
CSB_Modified_Full_String__c: 1, // ISO 8601 date string
- CSB_Review_Item_ID__c: 1, // CSB Rebate ID w/ form/version ID (9 digits)
+ CSB_Review_Item_ID__c: 1, // CSB Rebate ID with form/version ID (9 digits)
Parent_Rebate_ID__c: 1, // CSB Rebate ID (6 digits)
- "Parent_CSB_Rebate__r.CSB_Rebate_Status__c": 1,
+ Record_Type_Name__c: 1, // 'CSB Funding Request' | 'CSB Payment Request' | 'CSB Closeout Request'
+ "Parent_CSB_Rebate__r.CSB_Funding_Request_Status__c": 1,
+ "Parent_CSB_Rebate__r.CSB_Payment_Request_Status__c": 1,
+ "Parent_CSB_Rebate__r.CSB_Closeout_Request_Status__c": 1,
}
)
.sort({ CreatedDate: -1 })
.execute(async (err, records) => ((await err) ? err : records));
+
+ return submissions;
}
/**
@@ -230,7 +275,7 @@ async function queryForApplicationSubmissionsStatuses(req, comboKeys) {
* @param {string} reviewItemId CSB Rebate ID with the form/version ID (9 digits)
* @returns {Promise