diff --git a/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalCalculations.graphql b/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalCalculations.graphql
index ad179ef2dd..6cdc1728e8 100644
--- a/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalCalculations.graphql
+++ b/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalCalculations.graphql
@@ -18,6 +18,8 @@ fragment PdsGoalCalculationFields on DesignationSupportCalculation {
conferenceRetreatCosts
ministryTravelMeals
otherAnnualReimbursements
+ totalReimbursableExpenses
+ totalMonthlySupportGoal
}
query PdsGoalCalculations($after: String) {
diff --git a/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalsList.test.tsx b/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalsList.test.tsx
index fb1944c563..b431f65ab3 100644
--- a/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalsList.test.tsx
+++ b/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalsList.test.tsx
@@ -1,10 +1,6 @@
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import {
- MpdGoalMiscConstantCategoryEnum,
- MpdGoalMiscConstantLabelEnum,
-} from 'src/graphql/types.generated';
import { PdsGoalCalculatorTestWrapper } from '../PdsGoalCalculatorTestWrapper';
import { PdsGoalsList } from './PdsGoalsList';
@@ -49,44 +45,18 @@ describe('PdsGoalsList', () => {
expect(queryByTestId('goal-name')).not.toBeInTheDocument();
});
- it('seeds reimbursable defaults from MPD constants on create', async () => {
+ it('creates a new goal with empty attributes so the server applies defaults', async () => {
const { findByRole } = render(
-
+
,
);
- const button = await findByRole('button', { name: 'Create a New Goal' });
- await waitFor(() => {
- expect(button).toBeEnabled();
- });
- userEvent.click(button);
+ userEvent.click(await findByRole('button', { name: 'Create a New Goal' }));
await waitFor(() => {
expect(mutationSpy).toHaveGraphqlOperation('CreatePdsGoalCalculation', {
- attributes: {
- ministryCellPhone: 75,
- ministryInternet: 50,
- },
+ attributes: {},
});
});
});
diff --git a/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalsList.tsx b/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalsList.tsx
index b6175a06b5..074a6809ca 100644
--- a/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalsList.tsx
+++ b/src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalsList.tsx
@@ -5,7 +5,6 @@ import { useTranslation } from 'react-i18next';
import { useGetUserQuery } from 'src/components/User/GetUser.generated';
import { useAccountListId } from 'src/hooks/useAccountListId';
import { useFetchAllPages } from 'src/hooks/useFetchAllPages';
-import { useGoalCalculatorConstants } from 'src/hooks/useGoalCalculatorConstants';
import illustration6graybg from 'src/images/drawkit/grape/drawkit-grape-pack-illustration-6-gray-bg.svg';
import { PdsGoalCard } from '../GoalCard/PdsGoalCard';
import {
@@ -41,8 +40,6 @@ export const PdsGoalsList: React.FC = () => {
});
const [createPdsGoalCalculation] = useCreatePdsGoalCalculationMutation();
const [deletePdsGoalCalculation] = useDeletePdsGoalCalculationMutation();
- const { goalMiscConstants, loading: constantsLoading } =
- useGoalCalculatorConstants();
const goals = data?.designationSupportCalculations.nodes;
@@ -58,14 +55,7 @@ export const PdsGoalsList: React.FC = () => {
const handleCreateGoal = async () => {
const { data } = await createPdsGoalCalculation({
- variables: {
- attributes: {
- ministryCellPhone:
- goalMiscConstants.REIMBURSEMENTS_WITH_MAXIMUM?.PHONE?.fee,
- ministryInternet:
- goalMiscConstants.REIMBURSEMENTS_WITH_MAXIMUM?.INTERNET?.fee,
- },
- },
+ variables: { attributes: {} },
});
const calculation =
data?.createDesignationSupportCalculation?.designationSupportCalculation;
@@ -81,11 +71,7 @@ export const PdsGoalsList: React.FC = () => {
-
diff --git a/src/components/Reports/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx b/src/components/Reports/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx
index 54814c6cd4..194b5e435f 100644
--- a/src/components/Reports/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx
+++ b/src/components/Reports/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx
@@ -197,6 +197,12 @@ export const PdsGoalCalculatorTestWrapper: React.FC<
label: MpdGoalMiscConstantLabelEnum.AdminRate,
fee: 0.12,
},
+ {
+ category:
+ MpdGoalMiscConstantCategoryEnum.AdditionalRates,
+ label: MpdGoalMiscConstantLabelEnum.MinimumReimbursable,
+ fee: 300,
+ },
],
},
constantsMock,
diff --git a/src/components/Reports/PdsGoalCalculator/ReimbursableExpenses/MonthlyReimbursableSection.test.tsx b/src/components/Reports/PdsGoalCalculator/ReimbursableExpenses/MonthlyReimbursableSection.test.tsx
index 7ec665a3bd..fbddb4f33e 100644
--- a/src/components/Reports/PdsGoalCalculator/ReimbursableExpenses/MonthlyReimbursableSection.test.tsx
+++ b/src/components/Reports/PdsGoalCalculator/ReimbursableExpenses/MonthlyReimbursableSection.test.tsx
@@ -21,6 +21,9 @@ const TestComponent: React.FC = () => (
mpdMiscellaneous: 10,
accountTransfers: 50,
otherMonthlyReimbursements: 10,
+ conferenceRetreatCosts: 0,
+ ministryTravelMeals: 0,
+ otherAnnualReimbursements: 0,
}}
constantsMock={{
mpdGoalMiscConstants: [
@@ -34,6 +37,11 @@ const TestComponent: React.FC = () => (
label: MpdGoalMiscConstantLabelEnum.Internet,
fee: 30,
},
+ {
+ category: MpdGoalMiscConstantCategoryEnum.AdditionalRates,
+ label: MpdGoalMiscConstantLabelEnum.MinimumReimbursable,
+ fee: 0,
+ },
],
}}
>
@@ -70,14 +78,18 @@ describe('MonthlyReimbursableSection', () => {
);
});
- it('autosaves a valid amount edit', async () => {
+ it('autosaves a valid amount edit along with the recalculated total', async () => {
const { findByRole } = render();
await editAmountCell(findByRole, 'Ministry Cell Phone', '20');
+ // monthly = 20 + 30 + 25 + 10 + 50 + 10 = 145; floor = 0 (no constant set)
await waitFor(() =>
expect(mutationSpy).toHaveGraphqlOperation('UpdatePdsGoalCalculation', {
- attributes: { id: 'goal-1', ministryCellPhone: 20 },
+ attributes: {
+ ministryCellPhone: 20,
+ totalReimbursableExpenses: 145,
+ },
}),
);
});
diff --git a/src/components/Reports/PdsGoalCalculator/ReimbursableExpenses/TotalReimbursableSection.test.tsx b/src/components/Reports/PdsGoalCalculator/ReimbursableExpenses/TotalReimbursableSection.test.tsx
index 269ad38336..cff90ef264 100644
--- a/src/components/Reports/PdsGoalCalculator/ReimbursableExpenses/TotalReimbursableSection.test.tsx
+++ b/src/components/Reports/PdsGoalCalculator/ReimbursableExpenses/TotalReimbursableSection.test.tsx
@@ -1,6 +1,10 @@
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
+import {
+ MpdGoalMiscConstantCategoryEnum,
+ MpdGoalMiscConstantLabelEnum,
+} from 'src/graphql/types.generated';
import { PdsGoalCalculatorTestWrapper } from '../PdsGoalCalculatorTestWrapper';
import { TotalReimbursableSection } from './TotalReimbursableSection';
@@ -26,6 +30,15 @@ const TestComponent: React.FC = ({
ministryTravelMeals: 0,
otherAnnualReimbursements: 0,
}}
+ constantsMock={{
+ mpdGoalMiscConstants: [
+ {
+ category: MpdGoalMiscConstantCategoryEnum.AdditionalRates,
+ label: MpdGoalMiscConstantLabelEnum.MinimumReimbursable,
+ fee: 300,
+ },
+ ],
+ }}
>
diff --git a/src/components/Reports/PdsGoalCalculator/ReimbursableExpenses/TotalReimbursableSection.tsx b/src/components/Reports/PdsGoalCalculator/ReimbursableExpenses/TotalReimbursableSection.tsx
index 846a2ca8ba..4e41d22760 100644
--- a/src/components/Reports/PdsGoalCalculator/ReimbursableExpenses/TotalReimbursableSection.tsx
+++ b/src/components/Reports/PdsGoalCalculator/ReimbursableExpenses/TotalReimbursableSection.tsx
@@ -8,13 +8,11 @@ import {
styled,
} from '@mui/material';
import { useTranslation } from 'react-i18next';
+import { useGoalCalculatorConstants } from 'src/hooks/useGoalCalculatorConstants';
import { useLocale } from 'src/hooks/useLocale';
import { currencyFormat } from 'src/lib/intlFormat';
import { usePdsGoalCalculator } from '../Shared/PdsGoalCalculatorContext';
-import {
- REIMBURSABLE_FLOOR,
- calculateReimbursableTotals,
-} from '../calculations/reimbursableExpenses';
+import { calculateReimbursableTotals } from '../calculations/reimbursableExpenses';
const AmountTypography = styled(Typography)(({ theme }) => ({
fontSize: '2.5rem',
@@ -26,12 +24,15 @@ export const TotalReimbursableSection: React.FC = () => {
const { t } = useTranslation();
const locale = useLocale();
const { calculation } = usePdsGoalCalculator();
+ const { goalMiscConstants } = useGoalCalculatorConstants();
+ const reimbursableFloor =
+ goalMiscConstants.ADDITIONAL_RATES?.MINIMUM_REIMBURSABLE?.fee ?? 0;
if (!calculation) {
return null;
}
- const { total } = calculateReimbursableTotals(calculation);
+ const { total } = calculateReimbursableTotals(calculation, reimbursableFloor);
return (
@@ -44,7 +45,7 @@ export const TotalReimbursableSection: React.FC = () => {
title={t(
'The total is the greater of the {{floor}} minimum or your calculated amount.',
{
- floor: currencyFormat(REIMBURSABLE_FLOOR, 'USD', locale),
+ floor: currencyFormat(reimbursableFloor, 'USD', locale),
},
)}
>
diff --git a/src/components/Reports/PdsGoalCalculator/Shared/Autosave/useSaveField.test.tsx b/src/components/Reports/PdsGoalCalculator/Shared/Autosave/useSaveField.test.tsx
index 6b2fba668b..eab7c4963c 100644
--- a/src/components/Reports/PdsGoalCalculator/Shared/Autosave/useSaveField.test.tsx
+++ b/src/components/Reports/PdsGoalCalculator/Shared/Autosave/useSaveField.test.tsx
@@ -1,26 +1,48 @@
import React from 'react';
import { renderHook, waitFor } from '@testing-library/react';
-import { PdsGoalCalculatorTestWrapper } from '../../PdsGoalCalculatorTestWrapper';
+import {
+ DesignationSupportSalaryType,
+ MpdGoalMiscConstantCategoryEnum,
+ MpdGoalMiscConstantLabelEnum,
+} from 'src/graphql/types.generated';
+import {
+ GoalCalculatorConstantsMock,
+ PdsGoalCalculationMock,
+ PdsGoalCalculatorTestWrapper,
+} from '../../PdsGoalCalculatorTestWrapper';
import { useSaveField } from './useSaveField';
const mutationSpy = jest.fn();
-const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
-
- {children}
-
-);
+const buildWrapper = (
+ calculationMock: PdsGoalCalculationMock,
+ constantsMock?: GoalCalculatorConstantsMock,
+): React.FC<{ children: React.ReactNode }> => {
+ const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
+
+ {children}
+
+ );
+ return Wrapper;
+};
describe('useSaveField', () => {
- it('should update the calculation when a value changes', async () => {
- const { result } = renderHook(useSaveField, { wrapper: Wrapper });
+ beforeEach(() => {
+ mutationSpy.mockClear();
+ });
+
+ it('updates the calculation when a value changes', async () => {
+ const { result } = renderHook(useSaveField, {
+ wrapper: buildWrapper({
+ id: 'goal-1',
+ name: 'Test Goal',
+ payRate: 50000,
+ }),
+ });
await waitFor(() =>
expect(mutationSpy).toHaveGraphqlOperation('PdsGoalCalculation'),
@@ -28,11 +50,84 @@ describe('useSaveField', () => {
result.current({ name: 'New Name' });
+ await waitFor(() =>
+ expect(mutationSpy).toHaveGraphqlOperation('UpdatePdsGoalCalculation', {
+ attributes: { id: 'goal-1', name: 'New Name' },
+ }),
+ );
+ });
+
+ it('recomputes totalMonthlySupportGoal with the floored total when a salary input changes', async () => {
+ const { result } = renderHook(useSaveField, {
+ wrapper: buildWrapper({
+ id: 'goal-1',
+ salaryOrHourly: DesignationSupportSalaryType.Salaried,
+ payRate: 48000,
+ hoursWorkedPerWeek: null,
+ geographicLocation: null,
+ totalMonthlySupportGoal: 0,
+ }),
+ });
+
+ await waitFor(() =>
+ expect(mutationSpy).toHaveGraphqlOperation('PdsGoalCalculation'),
+ );
+
+ result.current({ payRate: 60000 });
+
+ // 60000 / 12 = 5000; FICA 0.08 -> 400; subtotal 5400
+ await waitFor(() =>
+ expect(mutationSpy).toHaveGraphqlOperation('UpdatePdsGoalCalculation', {
+ attributes: {
+ id: 'goal-1',
+ payRate: 60000,
+ totalMonthlySupportGoal: 5400,
+ },
+ }),
+ );
+ });
+
+ it('recomputes totalReimbursableExpenses with the floor applied when a reimbursable input changes', async () => {
+ const { result } = renderHook(useSaveField, {
+ wrapper: buildWrapper(
+ {
+ id: 'goal-1',
+ ministryCellPhone: 0,
+ ministryInternet: 0,
+ mpdNewsletter: 0,
+ mpdMiscellaneous: 0,
+ accountTransfers: 0,
+ otherMonthlyReimbursements: 0,
+ conferenceRetreatCosts: 0,
+ ministryTravelMeals: 0,
+ otherAnnualReimbursements: 0,
+ totalReimbursableExpenses: 0,
+ },
+ {
+ mpdGoalMiscConstants: [
+ {
+ category: MpdGoalMiscConstantCategoryEnum.AdditionalRates,
+ label: MpdGoalMiscConstantLabelEnum.MinimumReimbursable,
+ fee: 300,
+ },
+ ],
+ },
+ ),
+ });
+
+ await waitFor(() =>
+ expect(mutationSpy).toHaveGraphqlOperation('PdsGoalCalculation'),
+ );
+
+ result.current({ ministryCellPhone: 100 });
+
+ // raw 100 < 300 floor → total clamps to 300
await waitFor(() =>
expect(mutationSpy).toHaveGraphqlOperation('UpdatePdsGoalCalculation', {
attributes: {
id: 'goal-1',
- name: 'New Name',
+ ministryCellPhone: 100,
+ totalReimbursableExpenses: 300,
},
}),
);
diff --git a/src/components/Reports/PdsGoalCalculator/Shared/Autosave/useSaveField.ts b/src/components/Reports/PdsGoalCalculator/Shared/Autosave/useSaveField.ts
index 93ed87327f..b3f7d34704 100644
--- a/src/components/Reports/PdsGoalCalculator/Shared/Autosave/useSaveField.ts
+++ b/src/components/Reports/PdsGoalCalculator/Shared/Autosave/useSaveField.ts
@@ -2,9 +2,13 @@ import { useCallback } from 'react';
import { usePdsGoalCalculator } from 'src/components/Reports/PdsGoalCalculator/Shared/PdsGoalCalculatorContext';
import { DesignationSupportCalculationUpdateInput } from 'src/graphql/types.generated';
import { useUpdatePdsGoalCalculationMutation } from '../../GoalsList/PdsGoalCalculations.generated';
+import { calculateReimbursableTotals } from '../../calculations/reimbursableExpenses';
+import { calculateSalaryTotals } from '../../calculations/salaryCalculation';
export const useSaveField = () => {
- const { calculation, trackMutation } = usePdsGoalCalculator();
+ const { calculation, constants, trackMutation } = usePdsGoalCalculator();
+ const { reimbursableFloor, employerFicaRate, geographicMultipliers } =
+ constants;
const [updatePdsGoalCalculation] = useUpdatePdsGoalCalculationMutation();
const saveField = useCallback(
@@ -13,8 +17,32 @@ export const useSaveField = () => {
return;
}
- const unchanged = Object.keys(attributes).every(
- (key) => calculation[key] === attributes[key],
+ const merged = { ...calculation, ...attributes };
+ const derived: {
+ totalReimbursableExpenses?: number;
+ totalMonthlySupportGoal?: number;
+ } = {};
+
+ if (reimbursableFloor !== undefined) {
+ derived.totalReimbursableExpenses = calculateReimbursableTotals(
+ merged,
+ reimbursableFloor,
+ ).total;
+ }
+
+ if (employerFicaRate !== undefined) {
+ const geographicMultiplier =
+ geographicMultipliers.get(merged.geographicLocation ?? '') ?? 0;
+ derived.totalMonthlySupportGoal = calculateSalaryTotals(merged, {
+ geographicMultiplier,
+ employerFicaRate,
+ }).subtotal;
+ }
+
+ const payload = { ...attributes, ...derived };
+
+ const unchanged = Object.keys(payload).every(
+ (key) => calculation[key] === payload[key],
);
if (unchanged) {
return;
@@ -25,7 +53,7 @@ export const useSaveField = () => {
variables: {
attributes: {
id: calculation.id,
- ...attributes,
+ ...payload,
},
},
optimisticResponse: {
@@ -34,14 +62,27 @@ export const useSaveField = () => {
designationSupportCalculation: {
__typename: 'DesignationSupportCalculation',
...calculation,
- ...attributes,
+ ...payload,
+ totalReimbursableExpenses:
+ derived.totalReimbursableExpenses ??
+ calculation.totalReimbursableExpenses,
+ totalMonthlySupportGoal:
+ derived.totalMonthlySupportGoal ??
+ calculation.totalMonthlySupportGoal,
},
},
},
}),
);
},
- [calculation, trackMutation, updatePdsGoalCalculation],
+ [
+ calculation,
+ reimbursableFloor,
+ employerFicaRate,
+ geographicMultipliers,
+ trackMutation,
+ updatePdsGoalCalculation,
+ ],
);
return saveField;
diff --git a/src/components/Reports/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx b/src/components/Reports/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx
index 73a3eeb240..5e871955c3 100644
--- a/src/components/Reports/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx
+++ b/src/components/Reports/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx
@@ -2,6 +2,10 @@ import { useRouter } from 'next/router';
import React, { createContext, useCallback, useMemo, useState } from 'react';
import { useSnackbar } from 'notistack';
import { useTranslation } from 'react-i18next';
+import {
+ GoalGeographicConstantMap,
+ useGoalCalculatorConstants,
+} from 'src/hooks/useGoalCalculatorConstants';
import { useTrackMutation } from 'src/hooks/useTrackMutation';
import {
PdsGoalCalculationFieldsFragment,
@@ -11,6 +15,14 @@ import { PdsGoalCalculatorStepEnum } from '../PdsGoalCalculatorHelper';
import { HcmUserQuery, useHcmUserQuery } from './HCM.generated';
import { PdsGoalCalculatorStep, useSteps } from './useSteps';
+export interface PdsGoalCalculatorConstants {
+ reimbursableFloor: number | undefined;
+ employerFicaRate: number | undefined;
+ phoneMax: number | undefined;
+ internetMax: number | undefined;
+ geographicMultipliers: GoalGeographicConstantMap;
+}
+
export type PdsGoalCalculatorType = {
steps: PdsGoalCalculatorStep[];
currentStep: PdsGoalCalculatorStep;
@@ -18,6 +30,7 @@ export type PdsGoalCalculatorType = {
calculation?: PdsGoalCalculationFieldsFragment;
calculationLoading: boolean;
hcmUser?: HcmUserQuery['hcm'][number];
+ constants: PdsGoalCalculatorConstants;
/** Whether any mutations are currently in progress */
isMutating: boolean;
@@ -71,6 +84,21 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => {
const { data: hcmData } = useHcmUserQuery();
const hcmUser = hcmData?.hcm[0];
+ const { goalMiscConstants, goalGeographicConstantMap } =
+ useGoalCalculatorConstants();
+ const constants = useMemo(
+ () => ({
+ reimbursableFloor:
+ goalMiscConstants.ADDITIONAL_RATES?.MINIMUM_REIMBURSABLE?.fee,
+ employerFicaRate:
+ goalMiscConstants.ADDITIONAL_RATES?.EMPLOYER_FICA_RATE?.fee,
+ phoneMax: goalMiscConstants.REIMBURSEMENTS_WITH_MAXIMUM?.PHONE?.fee,
+ internetMax: goalMiscConstants.REIMBURSEMENTS_WITH_MAXIMUM?.INTERNET?.fee,
+ geographicMultipliers: goalGeographicConstantMap,
+ }),
+ [goalMiscConstants, goalGeographicConstantMap],
+ );
+
const steps = useSteps();
const [stepIndex, setStepIndex] = useState(0);
const [rightPanelContent, setRightPanelContent] =
@@ -121,6 +149,7 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => {
stepIndex,
calculation,
calculationLoading,
+ constants,
isMutating,
trackMutation,
hcmUser,
@@ -140,6 +169,7 @@ export const PdsGoalCalculatorProvider: React.FC = ({ children }) => {
stepIndex,
calculation,
calculationLoading,
+ constants,
isMutating,
trackMutation,
hcmUser,
diff --git a/src/components/Reports/PdsGoalCalculator/SupportItem/OtherSection.tsx b/src/components/Reports/PdsGoalCalculator/SupportItem/OtherSection.tsx
index 1f44f34b25..f0fbd93759 100644
--- a/src/components/Reports/PdsGoalCalculator/SupportItem/OtherSection.tsx
+++ b/src/components/Reports/PdsGoalCalculator/SupportItem/OtherSection.tsx
@@ -18,12 +18,12 @@ export const OtherSection: React.FC = () => {
const { t } = useTranslation();
const locale = useLocale();
const localeText = useDataGridLocaleText();
- const { calculation, hcmUser } = usePdsGoalCalculator();
- const { goalMiscConstants, goalGeographicConstantMap } =
- useGoalCalculatorConstants();
+ const { calculation, hcmUser, constants } = usePdsGoalCalculator();
+ const { reimbursableFloor, employerFicaRate, geographicMultipliers } =
+ constants;
+ const { goalMiscConstants } = useGoalCalculatorConstants();
const additionalRates = goalMiscConstants.ADDITIONAL_RATES;
- const employerFicaRate = additionalRates?.EMPLOYER_FICA_RATE?.fee;
const workCompPercentage = additionalRates?.PART_TIME_WORK_COMPENSATION?.fee;
const attritionRate = goalMiscConstants.RATES?.ATTRITION_RATE?.fee;
const creditCardFeeRate = additionalRates?.CREDIT_CARD_FEE_RATE?.fee;
@@ -33,6 +33,7 @@ export const OtherSection: React.FC = () => {
if (
!calculation ||
employerFicaRate === undefined ||
+ reimbursableFloor === undefined ||
workCompPercentage === undefined ||
attritionRate === undefined ||
creditCardFeeRate === undefined ||
@@ -42,13 +43,16 @@ export const OtherSection: React.FC = () => {
}
const geographicMultiplier =
- goalGeographicConstantMap.get(calculation.geographicLocation ?? '') ?? 0;
+ geographicMultipliers.get(calculation.geographicLocation ?? '') ?? 0;
const salaryTotals = calculateSalaryTotals(calculation, {
geographicMultiplier,
employerFicaRate,
});
- const reimbursableTotals = calculateReimbursableTotals(calculation);
+ const reimbursableTotals = calculateReimbursableTotals(
+ calculation,
+ reimbursableFloor,
+ );
const taxDeferredPct =
(hcmUser?.fourOThreeB?.currentTaxDeferredContributionPercentage ?? 0) /
@@ -71,12 +75,13 @@ export const OtherSection: React.FC = () => {
}, [
calculation,
hcmUser,
+ reimbursableFloor,
employerFicaRate,
workCompPercentage,
attritionRate,
creditCardFeeRate,
adminRate,
- goalGeographicConstantMap,
+ geographicMultipliers,
locale,
t,
]);
diff --git a/src/components/Reports/PdsGoalCalculator/calculations/reimbursableExpenses.test.ts b/src/components/Reports/PdsGoalCalculator/calculations/reimbursableExpenses.test.ts
index 383de9cf36..01b61d5d6c 100644
--- a/src/components/Reports/PdsGoalCalculator/calculations/reimbursableExpenses.test.ts
+++ b/src/components/Reports/PdsGoalCalculator/calculations/reimbursableExpenses.test.ts
@@ -1,9 +1,10 @@
import {
- REIMBURSABLE_FLOOR,
ReimbursableCalculationFields,
calculateReimbursableTotals,
} from './reimbursableExpenses';
+const FLOOR = 300;
+
const emptyCalculation: ReimbursableCalculationFields = {
ministryCellPhone: null,
ministryInternet: null,
@@ -18,72 +19,87 @@ const emptyCalculation: ReimbursableCalculationFields = {
describe('calculateReimbursableTotals', () => {
it('returns the floor when all fields are null', () => {
- const result = calculateReimbursableTotals(emptyCalculation);
+ const result = calculateReimbursableTotals(emptyCalculation, FLOOR);
expect(result.monthlySubtotal).toBe(0);
expect(result.annualSubtotal).toBe(0);
expect(result.raw).toBe(0);
- expect(result.total).toBe(REIMBURSABLE_FLOOR);
+ expect(result.total).toBe(FLOOR);
expect(result.floorApplied).toBe(true);
});
it('applies the floor when monthly subtotal is below 300 with no annual', () => {
- const result = calculateReimbursableTotals({
- ...emptyCalculation,
- ministryCellPhone: 100,
- ministryInternet: 150,
- });
+ const result = calculateReimbursableTotals(
+ {
+ ...emptyCalculation,
+ ministryCellPhone: 100,
+ ministryInternet: 150,
+ },
+ FLOOR,
+ );
expect(result.monthlySubtotal).toBe(250);
expect(result.raw).toBe(250);
- expect(result.total).toBe(REIMBURSABLE_FLOOR);
+ expect(result.total).toBe(FLOOR);
expect(result.floorApplied).toBe(true);
});
it('applies the floor at the exact boundary when raw equals the floor', () => {
- const result = calculateReimbursableTotals({
- ...emptyCalculation,
- ministryCellPhone: 150,
- ministryInternet: 150,
- });
- expect(result.raw).toBe(REIMBURSABLE_FLOOR);
- expect(result.total).toBe(REIMBURSABLE_FLOOR);
+ const result = calculateReimbursableTotals(
+ {
+ ...emptyCalculation,
+ ministryCellPhone: 150,
+ ministryInternet: 150,
+ },
+ FLOOR,
+ );
+ expect(result.raw).toBe(FLOOR);
+ expect(result.total).toBe(FLOOR);
expect(result.floorApplied).toBe(true);
});
it('returns the exact monthly subtotal when above the floor', () => {
- const result = calculateReimbursableTotals({
- ...emptyCalculation,
- ministryCellPhone: 200,
- ministryInternet: 200,
- });
+ const result = calculateReimbursableTotals(
+ {
+ ...emptyCalculation,
+ ministryCellPhone: 200,
+ ministryInternet: 200,
+ },
+ FLOOR,
+ );
expect(result.monthlySubtotal).toBe(400);
expect(result.total).toBe(400);
expect(result.floorApplied).toBe(false);
});
it('divides annual subtotal by 12 and applies the floor when result is below', () => {
- const result = calculateReimbursableTotals({
- ...emptyCalculation,
- conferenceRetreatCosts: 1200,
- });
+ const result = calculateReimbursableTotals(
+ {
+ ...emptyCalculation,
+ conferenceRetreatCosts: 1200,
+ },
+ FLOOR,
+ );
expect(result.annualSubtotal).toBe(1200);
expect(result.raw).toBe(100);
- expect(result.total).toBe(REIMBURSABLE_FLOOR);
+ expect(result.total).toBe(FLOOR);
expect(result.floorApplied).toBe(true);
});
it('combines monthly and annual contributions above the floor', () => {
- const result = calculateReimbursableTotals({
- ...emptyCalculation,
- ministryCellPhone: 50,
- ministryInternet: 50,
- mpdNewsletter: 25,
- mpdMiscellaneous: 25,
- accountTransfers: 50,
- otherMonthlyReimbursements: 50,
- conferenceRetreatCosts: 600,
- ministryTravelMeals: 600,
- otherAnnualReimbursements: 0,
- });
+ const result = calculateReimbursableTotals(
+ {
+ ...emptyCalculation,
+ ministryCellPhone: 50,
+ ministryInternet: 50,
+ mpdNewsletter: 25,
+ mpdMiscellaneous: 25,
+ accountTransfers: 50,
+ otherMonthlyReimbursements: 50,
+ conferenceRetreatCosts: 600,
+ ministryTravelMeals: 600,
+ otherAnnualReimbursements: 0,
+ },
+ FLOOR,
+ );
expect(result.monthlySubtotal).toBe(250);
expect(result.annualSubtotal).toBe(1200);
expect(result.raw).toBe(350);
@@ -92,12 +108,24 @@ describe('calculateReimbursableTotals', () => {
});
it('treats null fields as zero alongside non-null values', () => {
+ const result = calculateReimbursableTotals(
+ {
+ ...emptyCalculation,
+ ministryCellPhone: 500,
+ ministryInternet: null,
+ },
+ FLOOR,
+ );
+ expect(result.monthlySubtotal).toBe(500);
+ expect(result.total).toBe(500);
+ });
+
+ it('defaults the floor to zero when no floor is provided', () => {
const result = calculateReimbursableTotals({
...emptyCalculation,
- ministryCellPhone: 500,
- ministryInternet: null,
+ ministryCellPhone: 50,
});
- expect(result.monthlySubtotal).toBe(500);
- expect(result.total).toBe(500);
+ expect(result.total).toBe(50);
+ expect(result.floorApplied).toBe(false);
});
});
diff --git a/src/components/Reports/PdsGoalCalculator/calculations/reimbursableExpenses.ts b/src/components/Reports/PdsGoalCalculator/calculations/reimbursableExpenses.ts
index 6e4af57c9c..462d448376 100644
--- a/src/components/Reports/PdsGoalCalculator/calculations/reimbursableExpenses.ts
+++ b/src/components/Reports/PdsGoalCalculator/calculations/reimbursableExpenses.ts
@@ -1,7 +1,5 @@
import { DesignationSupportCalculation } from 'src/graphql/types.generated';
-export const REIMBURSABLE_FLOOR = 300;
-
export type ReimbursableCalculationFields = Pick<
DesignationSupportCalculation,
| 'ministryCellPhone'
@@ -25,6 +23,7 @@ export interface ReimbursableTotals {
export const calculateReimbursableTotals = (
calculation: ReimbursableCalculationFields,
+ floor = 0,
): ReimbursableTotals => {
const monthlySubtotal =
(calculation.ministryCellPhone ?? 0) +
@@ -38,12 +37,12 @@ export const calculateReimbursableTotals = (
(calculation.ministryTravelMeals ?? 0) +
(calculation.otherAnnualReimbursements ?? 0);
const raw = monthlySubtotal + annualSubtotal / 12;
- const total = Math.max(REIMBURSABLE_FLOOR, raw);
+ const total = Math.max(floor, raw);
return {
monthlySubtotal,
annualSubtotal,
raw,
total,
- floorApplied: raw <= REIMBURSABLE_FLOOR,
+ floorApplied: raw <= floor,
};
};