diff --git a/backend/openapi.yml b/backend/openapi.yml index 6a865ac939..38b4013227 100644 --- a/backend/openapi.yml +++ b/backend/openapi.yml @@ -1191,45 +1191,32 @@ paths: available_funding: type: string carry_forward_funding: - type: integer - can: - type: object + type: string + cans: + type: array properties: can: - type: array - items: - type: object - properties: - appropriation_term: - type: integer - authorizer_id: - type: integer - arrangement_type_id: - type: integer - number: - type: string - purpose: - type: string - managing_portfolio_id: - type: integer - nickname: - type: string - appropriation_date: - type: string - description: - type: string - id: - type: integer - expiration_date: - type: string - managing_project_id: - type: integer + properties: + active_period: + type: integer + description: + type: string + display_name: + type: string + id: + type: integer + nick_name: + type: string + number: + type: string + portfolio_id: + type: integer + projects: + type: array carry_forward_label: type: string - example: "" expiration_date: type: string - example: "" expected_funding: type: string in_draft_funding: @@ -1239,9 +1226,9 @@ paths: new_funding: type: string obligated_funding: - type: integer + type: string planned_funding: - type: integer + type: string received_funding: type: string total_funding: diff --git a/frontend/cypress/e2e/canDetail.cy.js b/frontend/cypress/e2e/canDetail.cy.js index 148642e622..ff60365116 100644 --- a/frontend/cypress/e2e/canDetail.cy.js +++ b/frontend/cypress/e2e/canDetail.cy.js @@ -13,6 +13,11 @@ afterEach(() => { const can502Nickname = "SSRD"; const can502Description = "Social Science Research and Development"; +const can504 = { + number: 504, + nickname: "G994426", + budgetAmount: "5_000_000" +}; const currentFiscalYear = getCurrentFiscalYear(); @@ -167,4 +172,53 @@ describe("CAN detail page", () => { .and("contain", "$6,000,000.00") .and("contain", "60%"); }); + it("handles budget form", () => { + cy.visit(`/cans/${can504.number}/funding`); + cy.get("#fiscal-year-select").select(currentFiscalYear); + cy.get("#edit").click(); + cy.get("#save-changes").should("be.disabled"); + cy.get("#add-fy-budget").should("be.disabled"); + cy.get("#carry-forward-card").should("contain", "0"); + cy.get("[data-cy='can-budget-fy-card']").should("contain", "0"); + cy.get("#budget-amount").type(can504.budgetAmount); + cy.get("#budget-amount").clear(); + cy.get(".usa-error-message").should("exist").contains("This is required information"); + cy.get("#budget-amount").type(can504.budgetAmount); + cy.get(".usa-error-message").should("not.exist"); + cy.get("#add-fy-budget").click(); + cy.get("[data-cy='can-budget-fy-card']").should("contain", "5,000,000.00"); + cy.get("#save-changes").should("be.enabled"); + cy.get("#save-changes").click(); + cy.get(".usa-alert__body").should("contain", `The CAN ${can504.nickname} has been successfully updated.`); + cy.get("[data-cy=budget-received-card]").should("exist").and("contain", "Received $0.00 of $5,000,000.00"); + cy.get("[data-cy=can-budget-fy-card]") + .should("exist") + .and("contain", "CAN Budget by FY") + .and("contain", `FY ${currentFiscalYear}`) + .and("contain", "$5,000,000.00"); + }); + it("handles cancelling from budget form", () => { + cy.visit(`/cans/${can504.number}/funding`); + cy.get("#fiscal-year-select").select(currentFiscalYear); + cy.get("#edit").click(); + cy.get("#carry-forward-card").should("contain", "0"); + cy.get("[data-cy='can-budget-fy-card']").should("contain", "5,000,000.00"); + cy.get("#budget-amount").type("6_000_000"); + cy.get("#add-fy-budget").click(); + cy.get("[data-cy='can-budget-fy-card']").should("contain", "6,000,000.00"); + cy.get("#save-changes").should("be.enabled"); + cy.get("[data-cy=cancel-button]").should("be.enabled"); + cy.get("[data-cy=cancel-button]").click(); + cy.get(".usa-modal__heading").should( + "contain", + "Are you sure you want to cancel editing? Your changes will not be saved." + ); + cy.get("[data-cy='confirm-action']").click(); + cy.get("[data-cy=budget-received-card]").should("exist").and("contain", "Received $0.00 of $5,000,000.00"); + cy.get("[data-cy=can-budget-fy-card]") + .should("exist") + .and("contain", "CAN Budget by FY") + .and("contain", `FY ${currentFiscalYear}`) + .and("contain", "$5,000,000.00"); + }); }); diff --git a/frontend/src/api/opsAPI.js b/frontend/src/api/opsAPI.js index 7007f513e6..1536f99d43 100644 --- a/frontend/src/api/opsAPI.js +++ b/frontend/src/api/opsAPI.js @@ -212,6 +212,24 @@ export const opsApi = createApi({ }), invalidatesTags: ["Cans"] }), + addCanFundingBudgets: builder.mutation({ + query: ({ data }) => ({ + url: `/can-funding-budgets/`, + method: "POST", + headers: { "Content-Type": "application/json" }, + body: data + }), + invalidatesTags: ["Cans", "CanFunding"] + }), + updateCanFundingBudget: builder.mutation({ + query: ({ id, data }) => ({ + url: `/can-funding-budgets/${id}`, + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: data + }), + invalidatesTags: ["Cans", "CanFunding"] + }), getCanFundingSummary: builder.query({ query: ({ ids, fiscalYear, activePeriod, transfer, portfolio, fyBudgets }) => { const queryParams = []; @@ -243,7 +261,7 @@ export const opsApi = createApi({ return `/can-funding-summary?${queryParams.join("&")}`; }, - providesTags: ["CanFunding"] + providesTags: ["Cans", "CanFunding"] }), getNotificationsByUserId: builder.query({ query: ({ id, auth_header }) => { @@ -331,8 +349,8 @@ export const opsApi = createApi({ invalidatesTags: ["ServicesComponents", "Agreements", "BudgetLineItems", "AgreementHistory"] }), getChangeRequestsList: builder.query({ - query: ({userId}) => ({ - url: `/change-requests/${userId ? `?userId=${userId}` : ""}`, + query: ({ userId }) => ({ + url: `/change-requests/${userId ? `?userId=${userId}` : ""}` }), providesTags: ["ChangeRequests"] }), @@ -413,6 +431,8 @@ export const { useGetCansQuery, useGetCanByIdQuery, useUpdateCanMutation, + useAddCanFundingBudgetsMutation, + useUpdateCanFundingBudgetMutation, useGetCanFundingSummaryQuery, useGetNotificationsByUserIdQuery, useGetNotificationsByUserIdAndAgreementIdQuery, diff --git a/frontend/src/components/CANs/CANBudgetForm/CANBudgetForm.jsx b/frontend/src/components/CANs/CANBudgetForm/CANBudgetForm.jsx new file mode 100644 index 0000000000..07f727ed60 --- /dev/null +++ b/frontend/src/components/CANs/CANBudgetForm/CANBudgetForm.jsx @@ -0,0 +1,60 @@ +import CurrencyInput from "../../UI/Form/CurrencyInput"; +import icons from "../../../uswds/img/sprite.svg"; + +/** + * @typedef {Object} CANBudgetFormProps + * @property {string} budgetAmount + * @property {(arg: string) => string} cn + * @property {Object} res + * @property {number} fiscalYear + * @property {(e: React.FormEvent) => void} handleAddBudget + * @property {(name: string, value: string) => void} runValidate + * @property { React.Dispatch>} setBudgetAmount + */ + +/** + * @component - The CAN Budget Form component. + * @param {CANBudgetFormProps} props + * @returns {JSX.Element} - The component JSX. + */ +const CANBudgetForm = ({ budgetAmount, cn, res, fiscalYear, handleAddBudget, runValidate, setBudgetAmount }) => { + const fillColor = budgetAmount ? "#005ea2" : "#757575"; + + return ( +
{ + handleAddBudget(e); + setBudgetAmount(""); + }} + > +
+ { + runValidate("budget-amount", value); + }} + setEnteredAmount={setBudgetAmount} + value={budgetAmount || ""} + messages={res.getErrors("budget-amount")} + className={cn("budget-amount")} + /> +
+ +
+ ); +}; +export default CANBudgetForm; diff --git a/frontend/src/components/CANs/CANBudgetForm/CANBudgetForm.test.jsx b/frontend/src/components/CANs/CANBudgetForm/CANBudgetForm.test.jsx new file mode 100644 index 0000000000..7c8da82f0d --- /dev/null +++ b/frontend/src/components/CANs/CANBudgetForm/CANBudgetForm.test.jsx @@ -0,0 +1,72 @@ +import { describe, test, expect, vi } from "vitest"; +import { render, screen, fireEvent } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import CANBudgetForm from "./CANBudgetForm"; + +describe("CANBudgetForm", () => { + const defaultProps = { + budgetAmount: "", + cn: (name) => name, + res: { getErrors: () => [] }, + fiscalYear: 2024, + handleAddBudget: vi.fn(), + runValidate: vi.fn(), + setBudgetAmount: vi.fn() + }; + + test("renders with required props", () => { + render(); + expect(screen.getByLabelText(/FY 2024 CAN Budget/i)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: /add fy budget/i })).toBeInTheDocument(); + }); + + test("button is disabled when budgetAmount is empty", () => { + render(); + expect(screen.getByRole("button", { name: /add fy budget/i })).toBeDisabled(); + }); + + test("button is enabled when budgetAmount has value", () => { + render( + + ); + expect(screen.getByRole("button", { name: /add fy budget/i })).toBeEnabled(); + }); + + test("calls handleAddBudget and setBudgetAmount on form submission", async () => { + const user = userEvent.setup(); + render( + + ); + + await user.click(screen.getByRole("button", { name: /add fy budget/i })); + + expect(defaultProps.handleAddBudget).toHaveBeenCalled(); + expect(defaultProps.setBudgetAmount).toHaveBeenCalledWith(""); + }); + + test("calls runValidate when currency input changes", () => { + render(); + + fireEvent.change(screen.getByLabelText(/FY 2024 CAN Budget/i), { + target: { value: "1000" } + }); + + expect(defaultProps.runValidate).toHaveBeenCalledWith("budget-amount", "1,000"); + }); + + test("displays validation errors when present", () => { + const propsWithError = { + ...defaultProps, + res: { getErrors: () => ["This is required information"] } + }; + + render(); + expect(screen.getByText("This is required information")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/CANs/CANBudgetForm/index.js b/frontend/src/components/CANs/CANBudgetForm/index.js new file mode 100644 index 0000000000..1d8df62e6b --- /dev/null +++ b/frontend/src/components/CANs/CANBudgetForm/index.js @@ -0,0 +1 @@ +export {default} from "./CANBudgetForm" diff --git a/frontend/src/components/CANs/CANBudgetForm/suite.js b/frontend/src/components/CANs/CANBudgetForm/suite.js new file mode 100644 index 0000000000..5ec1e807fa --- /dev/null +++ b/frontend/src/components/CANs/CANBudgetForm/suite.js @@ -0,0 +1,11 @@ +import { create, test, enforce, only } from "vest"; + +const suite = create((data = {}, fieldName) => { + only(fieldName); + + test("budget-amount", "This is required information", () => { + enforce(data["budget-amount"]).isNotBlank(); + }); +}); + +export default suite; diff --git a/frontend/src/components/CANs/CANDetailForm/CANDetailForm.hooks.js b/frontend/src/components/CANs/CANDetailForm/CANDetailForm.hooks.js index 9ddeb84dbe..0ed8c9ab08 100644 --- a/frontend/src/components/CANs/CANDetailForm/CANDetailForm.hooks.js +++ b/frontend/src/components/CANs/CANDetailForm/CANDetailForm.hooks.js @@ -3,7 +3,15 @@ import classnames from "vest/classnames"; import { useUpdateCanMutation } from "../../../api/opsAPI"; import useAlert from "../../../hooks/use-alert.hooks"; import suite from "./suite.js"; - +/** + * @description - Custom hook for the CAN Detail Form. + * @param {number} canId + * @param {string} canNumber + * @param {string} canNickname + * @param {string} canDescription + * @param {number} portfolioId + * @param {() => void} toggleEditMode + */ export default function useCanDetailForm(canId, canNumber, canNickname, canDescription, portfolioId, toggleEditMode) { const [nickName, setNickName] = React.useState(canNickname); const [description, setDescription] = React.useState(canDescription); @@ -70,6 +78,7 @@ export default function useCanDetailForm(canId, canNumber, canNickname, canDescr handleConfirm: () => {} }); toggleEditMode(); + suite.reset(); }; const runValidate = (name, value) => { diff --git a/frontend/src/components/CANs/CANFundingReceivedTable/CANFundingReceivedTable.jsx b/frontend/src/components/CANs/CANFundingReceivedTable/CANFundingReceivedTable.jsx index 5127b9d4e1..cd86be2343 100644 --- a/frontend/src/components/CANs/CANFundingReceivedTable/CANFundingReceivedTable.jsx +++ b/frontend/src/components/CANs/CANFundingReceivedTable/CANFundingReceivedTable.jsx @@ -17,7 +17,7 @@ import { NO_DATA } from "../../../constants"; /** * @typedef {Object} CANFundingReceivedTableProps - * @property {number} totalFunding + * @property {string} totalFunding * @property {FundingReceived[]} fundingReceived data for table */ diff --git a/frontend/src/components/CANs/CANTypes.d.ts b/frontend/src/components/CANs/CANTypes.d.ts index e3beb41bb0..e323a5438c 100644 --- a/frontend/src/components/CANs/CANTypes.d.ts +++ b/frontend/src/components/CANs/CANTypes.d.ts @@ -35,6 +35,7 @@ export type BasicCAN = { nick_name?: string; number: string; portfolio_id: number; + projects: Project[]; }; export type URL = { @@ -105,3 +106,23 @@ export type FundingReceived = { updated_by_user?: any; updated_on?: any; }; + +export type FundingSummary = { + available_funding: string; + cans: FundingSummaryCAN[]; + carry_forward_funding: string; + expected_funding: string; + in_draft_funding: string; + in_execution_funding: string; + new_funding: string; + obligated_funding: string; + planned_funding: string; + received_funding: string; + total_funding: string; +}; + +export type FundingSummaryCAN = { + can: BasicCAN; + carry_forward_label: string; + expiration_date: string; +}; diff --git a/frontend/src/components/UI/Cards/BudgetCard/ReceivedFundingCard.jsx b/frontend/src/components/UI/Cards/BudgetCard/ReceivedFundingCard.jsx index 1bc36254d2..7cb6eff97d 100644 --- a/frontend/src/components/UI/Cards/BudgetCard/ReceivedFundingCard.jsx +++ b/frontend/src/components/UI/Cards/BudgetCard/ReceivedFundingCard.jsx @@ -7,8 +7,8 @@ import Tag from "../../Tag"; /** * @typedef {Object} BudgetCardProps * @property {string} title - The title of the card. - * @property {number} totalReceived - The total received. - * @property {number} totalFunding - The total funding. + * @property {string} totalReceived - The total received. + * @property {string} totalFunding - The total funding. * @property {string} [tagText] - The text for the tag. * @property {string} [helperText] - The helper text. */ diff --git a/frontend/src/components/UI/Form/CurrencyInput/CurrencyInput.jsx b/frontend/src/components/UI/Form/CurrencyInput/CurrencyInput.jsx index 756939f786..9983a8a43f 100644 --- a/frontend/src/components/UI/Form/CurrencyInput/CurrencyInput.jsx +++ b/frontend/src/components/UI/Form/CurrencyInput/CurrencyInput.jsx @@ -10,7 +10,7 @@ import CurrencyFormat from "react-currency-format"; * @param {Function} props.onChange - A function to call when the input value changes. * @param {boolean} [props.pending] - A flag to indicate if the input is pending (optional). * @param {Array} [props.messages] - An array of error messages to display (optional). - * @param {string} [props.value] - The value of the input field.(optional) + * @param {string | number} [props.value] - The value of the input field.(optional) * @param {string} [props.className] - Additional CSS classes to apply to the component (optional). * @param {Function} [props.setEnteredAmount] - A function to call when the input value changes. * @returns {JSX.Element} - The rendered component. diff --git a/frontend/src/pages/cans/detail/Can.hooks.js b/frontend/src/pages/cans/detail/Can.hooks.js index 072fef1c46..abba8769a3 100644 --- a/frontend/src/pages/cans/detail/Can.hooks.js +++ b/frontend/src/pages/cans/detail/Can.hooks.js @@ -3,14 +3,20 @@ import { useSelector } from "react-redux"; import { useParams } from "react-router-dom"; import { useGetCanByIdQuery, useGetCanFundingSummaryQuery } from "../../../api/opsAPI"; import { USER_ROLES } from "../../../components/Users/User.constants"; -import { getTypesCounts } from "./Can.helpers"; import { NO_DATA } from "../../../constants"; +import { getTypesCounts } from "./Can.helpers"; export default function useCan() { /** * @typedef {import("../../../components/CANs/CANTypes").CAN} CAN + * @typedef {import("../../../components/CANs/CANTypes").FundingSummary} FundingSummary */ + // check CAN Funding for current fiscal year + // send to CanFunding hook + // if its present PATCH otherwise POST + + const [isEditMode, setIsEditMode] = React.useState(false); const activeUser = useSelector((state) => state.auth.activeUser); const userRoles = activeUser?.roles ?? []; const isBudgetTeam = userRoles.includes(USER_ROLES.BUDGET_TEAM); @@ -19,10 +25,15 @@ export default function useCan() { const urlPathParams = useParams(); const canId = parseInt(urlPathParams.id ?? "-1"); /** @type {{data?: CAN | undefined, isLoading: boolean}} */ - const { data: can, isLoading } = useGetCanByIdQuery(canId); + + const { data: can, isLoading } = useGetCanByIdQuery(canId, { + refetchOnMountOrArgChange: true + }); + /** @type {{data?: FundingSummary | undefined, isLoading: boolean}} */ const { data: CANFunding, isLoading: CANFundingLoading } = useGetCanFundingSummaryQuery({ ids: [canId], - fiscalYear: fiscalYear + fiscalYear: fiscalYear, + refetchOnMountOrArgChange: true }); const budgetLineItemsByFiscalYear = React.useMemo(() => { @@ -65,8 +76,15 @@ export default function useCan() { [fiscalYear, can] ); + const toggleEditMode = () => { + setIsEditMode(!isEditMode); + }; + + const currentFiscalYearFundingId = can?.funding_budgets?.find((funding) => funding.fiscal_year === fiscalYear)?.id; + return { can: can ?? null, + currentFiscalYearFundingId, isLoading, canId, fiscalYear, @@ -82,17 +100,20 @@ export default function useCan() { teamLeaders: can?.portfolio?.team_leaders ?? [], portfolioName: can?.portfolio?.name, portfolioId: can?.portfolio_id ?? -1, - totalFunding: CANFunding?.total_funding, - plannedFunding: CANFunding?.planned_funding, - obligatedFunding: CANFunding?.obligated_funding, - inExecutionFunding: CANFunding?.in_execution_funding, - inDraftFunding: CANFunding?.in_draft_funding, - expectedFunding: CANFunding?.expected_funding, - receivedFunding: CANFunding?.received_funding, + totalFunding: CANFunding?.total_funding ?? "0", + plannedFunding: CANFunding?.planned_funding ?? "0", + obligatedFunding: CANFunding?.obligated_funding ?? "0", + inExecutionFunding: CANFunding?.in_execution_funding ?? "0", + inDraftFunding: CANFunding?.in_draft_funding ?? "0", + receivedFunding: CANFunding?.received_funding ?? "0", + carryForwardFunding: CANFunding?.carry_forward_funding ?? "0", subTitle: can?.nick_name ?? "", projectTypesCount, budgetLineTypesCount, agreementTypesCount, - isBudgetTeam + isBudgetTeam, + toggleEditMode, + isEditMode, + setIsEditMode }; } diff --git a/frontend/src/pages/cans/detail/Can.jsx b/frontend/src/pages/cans/detail/Can.jsx index d3dad9b144..fa17cb7cae 100644 --- a/frontend/src/pages/cans/detail/Can.jsx +++ b/frontend/src/pages/cans/detail/Can.jsx @@ -16,6 +16,7 @@ import CanSpending from "./CanSpending"; const Can = () => { const { can, + currentFiscalYearFundingId, isLoading, canId, fiscalYear, @@ -41,7 +42,10 @@ const Can = () => { budgetLineTypesCount, agreementTypesCount, receivedFunding, - isBudgetTeam + isBudgetTeam, + carryForwardFunding, + isEditMode, + toggleEditMode } = useCan(); if (isLoading || CANFundingLoading) { @@ -61,10 +65,12 @@ const Can = () => {
- + {!isEditMode && ( + + )}
{ teamLeaders={teamLeaders ?? []} fiscalYear={fiscalYear} isBudgetTeamMember={isBudgetTeam} + isEditMode={isEditMode} + toggleEditMode={toggleEditMode} /> } /> @@ -105,12 +113,19 @@ const Can = () => { path="funding" element={ } /> diff --git a/frontend/src/pages/cans/detail/CanDetail.jsx b/frontend/src/pages/cans/detail/CanDetail.jsx index a163af88c1..8485986875 100644 --- a/frontend/src/pages/cans/detail/CanDetail.jsx +++ b/frontend/src/pages/cans/detail/CanDetail.jsx @@ -1,6 +1,5 @@ import { faPen } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React from "react"; import { useGetDivisionQuery } from "../../../api/opsAPI"; import CANDetailForm from "../../../components/CANs/CANDetailForm"; import CANDetailView from "../../../components/CANs/CANDetailView"; @@ -24,6 +23,8 @@ import useGetUserFullNameFromId from "../../../hooks/user.hooks"; * @property {number} divisionId * @property {number} fiscalYear * @property {boolean} isBudgetTeamMember + * @property {boolean} isEditMode + * @property {() => void} toggleEditMode */ /** @@ -41,15 +42,12 @@ const CanDetail = ({ teamLeaders, divisionId, fiscalYear, - isBudgetTeamMember + isBudgetTeamMember, + isEditMode, + toggleEditMode }) => { const { data: division, isSuccess } = useGetDivisionQuery(divisionId); const divisionDirectorFullName = useGetUserFullNameFromId(isSuccess ? division.division_director_id : null); - const [isEditMode, setIsEditMode] = React.useState(false); - - const toggleEditMode = () => { - setIsEditMode(!isEditMode); - }; const currentFiscalYear = getCurrentFiscalYear(); const showButton = isBudgetTeamMember && fiscalYear === Number(currentFiscalYear); @@ -59,7 +57,7 @@ const CanDetail = ({

{!isEditMode ? "CAN Details" : "Edit CAN Details"}

- {showButton && ( + {showButton && !isEditMode && ( + )} +
+

+ {!isEditMode + ? "The summary below shows the funding for this CAN for the select fiscal year." + : "Review the new FY Funding Information for this CAN."} +

-
-
- - -
-
-

- * For multi-year CANs, the total budget will display in the first year, and the carry-forward - will display in every year after -

-

- * TBD means the FY Budget has not yet been entered by the Budget Team -

-
-
+ {!isEditMode ? ( +
+
+ + +
+
+

+ * For multi-year CANs, the total budget will display in the first year, and the + carry-forward will display in every year after +

+

+ * TBD means the FY Budget has not yet been entered by the Budget Team +

+
+
+ ) : ( +
+

{`Add FY ${fiscalYear} CAN Budget`}

+

{`Enter the FY ${fiscalYear} CAN Budget that teams will utilize for planning. For Multi-Year CANs, the Previous FYs Carry-Forward will display for you to review and enter as-is or edit, if needed.`}

+
+
+ +

Previous FYs Carry Forward

+ +
+ +
+ +
+
+ )} )} + {isEditMode && ( +
+ + +
+ )} ); }; diff --git a/frontend/src/pages/cans/detail/CanSpending.jsx b/frontend/src/pages/cans/detail/CanSpending.jsx index ff1f61052c..3396fce638 100644 --- a/frontend/src/pages/cans/detail/CanSpending.jsx +++ b/frontend/src/pages/cans/detail/CanSpending.jsx @@ -22,11 +22,11 @@ import { calculatePercent } from "../../../helpers/utils"; * @property {ItemCount[]} [projectTypesCount] * @property {ItemCount[]} [budgetLineTypesCount] * @property {ItemCount[]} [agreementTypesCount] - * @property {number} plannedFunding - * @property {number} inExecutionFunding - * @property {number} inDraftFunding - * @property {number} obligatedFunding - * @property {number} totalFunding + * @property {string} plannedFunding + * @property {string} inExecutionFunding + * @property {string} inDraftFunding + * @property {string} obligatedFunding + * @property {string} totalFunding */ /**