Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ fragment PdsGoalCalculationFields on DesignationSupportCalculation {
conferenceRetreatCosts
ministryTravelMeals
otherAnnualReimbursements
totalReimbursableExpenses
totalMonthlySupportGoal
}

query PdsGoalCalculations($after: String) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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(
<PdsGoalCalculatorTestWrapper
withProvider={false}
onCall={mutationSpy}
constantsMock={{
mpdGoalMiscConstants: [
{
category:
MpdGoalMiscConstantCategoryEnum.ReimbursementsWithMaximum,
label: MpdGoalMiscConstantLabelEnum.Phone,
fee: 75,
},
{
category:
MpdGoalMiscConstantCategoryEnum.ReimbursementsWithMaximum,
label: MpdGoalMiscConstantLabelEnum.Internet,
fee: 50,
},
],
}}
>
<PdsGoalCalculatorTestWrapper withProvider={false} onCall={mutationSpy}>
<PdsGoalsList />
</PdsGoalCalculatorTestWrapper>,
);

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: {},
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
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 {
Expand Down Expand Up @@ -40,52 +39,39 @@
pageInfo: data?.designationSupportCalculations.pageInfo,
});
const [createPdsGoalCalculation] = useCreatePdsGoalCalculationMutation();
const [deletePdsGoalCalculation] = useDeletePdsGoalCalculationMutation();
const { goalMiscConstants, loading: constantsLoading } =
useGoalCalculatorConstants();

const goals = data?.designationSupportCalculations.nodes;

const handleDeleteGoal = async (id: string) => {
await deletePdsGoalCalculation({
variables: { id },
update: (cache) => {
cache.evict({ id: `DesignationSupportCalculation:${id}` });
cache.gc();
},
});
};

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;

if (calculation) {
router.push(
`/accountLists/${accountListId}/hrTools/pdsGoalCalculator/${calculation.id}`,
);
}
};

return (
<Container>
<PdsGoalsListWelcome firstName={firstName} />
<Stack direction="row" gap={2} pb={3}>
<Button
variant="contained"
onClick={handleCreateGoal}
disabled={constantsLoading}
>
<Button variant="contained" onClick={handleCreateGoal}>

Check warning on line 74 in src/components/Reports/PdsGoalCalculator/GoalsList/PdsGoalsList.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Complex Method

PdsGoalsList:React.FC has a cyclomatic complexity of 11, threshold = 10. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
{t('Create a New Goal')}
</Button>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@
label: MpdGoalMiscConstantLabelEnum.AdminRate,
fee: 0.12,
},
{
category:
MpdGoalMiscConstantCategoryEnum.AdditionalRates,
label: MpdGoalMiscConstantLabelEnum.MinimumReimbursable,
fee: 300,
},

Check warning on line 205 in src/components/Reports/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

PdsGoalCalculatorTestWrapper:React.FC<PdsGoalCalculatorTestWrapperProps> increases from 121 to 127 lines of code, threshold = 120. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
],
},
constantsMock,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const TestComponent: React.FC = () => (
mpdMiscellaneous: 10,
accountTransfers: 50,
otherMonthlyReimbursements: 10,
conferenceRetreatCosts: 0,
ministryTravelMeals: 0,
otherAnnualReimbursements: 0,
}}
constantsMock={{
mpdGoalMiscConstants: [
Expand All @@ -34,6 +37,11 @@ const TestComponent: React.FC = () => (
label: MpdGoalMiscConstantLabelEnum.Internet,
fee: 30,
},
{
category: MpdGoalMiscConstantCategoryEnum.AdditionalRates,
label: MpdGoalMiscConstantLabelEnum.MinimumReimbursable,
fee: 0,
},
],
}}
>
Expand Down Expand Up @@ -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(<TestComponent />);

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,
},
}),
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -26,6 +30,15 @@ const TestComponent: React.FC<Props> = ({
ministryTravelMeals: 0,
otherAnnualReimbursements: 0,
}}
constantsMock={{
mpdGoalMiscConstants: [
{
category: MpdGoalMiscConstantCategoryEnum.AdditionalRates,
label: MpdGoalMiscConstantLabelEnum.MinimumReimbursable,
fee: 300,
},
],
}}
>
<TotalReimbursableSection />
</PdsGoalCalculatorTestWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 (
<Card>
Expand All @@ -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),
},
)}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,133 @@
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 }) => (
<PdsGoalCalculatorTestWrapper
calculationMock={{
id: 'goal-1',
name: 'Test Goal',
payRate: 50000,
}}
onCall={mutationSpy}
>
{children}
</PdsGoalCalculatorTestWrapper>
);
const buildWrapper = (
calculationMock: PdsGoalCalculationMock,
constantsMock?: GoalCalculatorConstantsMock,
): React.FC<{ children: React.ReactNode }> => {
const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<PdsGoalCalculatorTestWrapper
calculationMock={calculationMock}
constantsMock={constantsMock}
onCall={mutationSpy}
>
{children}
</PdsGoalCalculatorTestWrapper>
);
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'),
);

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,
},
}),
);
Expand Down
Loading
Loading