Skip to content
Merged
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
@@ -1,7 +1,7 @@
import React from 'react';
import { ThemeProvider } from '@mui/material/styles';
import { MockLinkCallHandler } from 'graphql-ergonomock/dist/apollo/MockLink';
import { merge } from 'lodash';
import { merge, mergeWith } from 'lodash';
import { SnackbarProvider } from 'notistack';
import { DeepPartial } from 'ts-essentials';
import TestRouter from '__tests__/util/TestRouter';
Expand All @@ -10,6 +10,8 @@ import { GetUserQuery } from 'src/components/User/GetUser.generated';
import {
DesignationSupportSalaryType,
DesignationSupportStatus,
MpdGoalMiscConstantCategoryEnum,
MpdGoalMiscConstantLabelEnum,
} from 'src/graphql/types.generated';
import { GoalCalculatorConstantsQuery } from 'src/hooks/goalCalculatorConstants.generated';
import theme from 'src/theme';
Expand Down Expand Up @@ -147,7 +149,8 @@ export const PdsGoalCalculatorTestWrapper: React.FC<
},
...(userMock ? { GetUser: userMock } : {}),
GoalCalculatorConstants: {
constant: merge(
constant: mergeWith(
{},
{
mpdGoalBenefitsConstants: [],
mpdGoalGeographicConstants: [
Expand All @@ -164,9 +167,18 @@ export const PdsGoalCalculatorTestWrapper: React.FC<
percentageMultiplier: 0.12,
},
],
mpdGoalMiscConstants: [],
mpdGoalMiscConstants: [
{
category:
MpdGoalMiscConstantCategoryEnum.AdditionalRates,
label: MpdGoalMiscConstantLabelEnum.EmployerFicaRate,
fee: 0.08,
},
],
},
constantsMock,
(_objValue, srcValue) =>
Array.isArray(srcValue) ? srcValue : undefined,
),
},
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { usePdsGoalCalculator } from '../Shared/PdsGoalCalculatorContext';
import { calculateReimbursableTotals } from '../calculations/reimbursableExpenses';
import {
ReimbursableExpensesGrid,
ReimbursableField,
} from './ReimbursableExpensesGrid';
import { calculateReimbursableTotals } from './reimbursableExpenses';

export const AnnualReimbursableSection: React.FC = () => {
const { t } = useTranslation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { useGoalCalculatorConstants } from 'src/hooks/useGoalCalculatorConstants';
import { usePdsGoalCalculator } from '../Shared/PdsGoalCalculatorContext';
import { calculateReimbursableTotals } from '../calculations/reimbursableExpenses';
import {
ReimbursableExpensesGrid,
ReimbursableField,
} from './ReimbursableExpensesGrid';
import { calculateReimbursableTotals } from './reimbursableExpenses';

export const MonthlyReimbursableSection: React.FC = () => {
const { t } = useTranslation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useLocale } from 'src/hooks/useLocale';
import { currencyFormat } from 'src/lib/intlFormat';
import { useSaveField } from '../Shared/Autosave/useSaveField';
import { usePdsGoalCalculator } from '../Shared/PdsGoalCalculatorContext';
import { ReimbursableCalculationFields } from './reimbursableExpenses';
import { ReimbursableCalculationFields } from '../calculations/reimbursableExpenses';

export type ReimbursableFieldName = keyof ReimbursableCalculationFields;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { usePdsGoalCalculator } from '../Shared/PdsGoalCalculatorContext';
import {
REIMBURSABLE_FLOOR,
calculateReimbursableTotals,
} from './reimbursableExpenses';
} from '../calculations/reimbursableExpenses';

const AmountTypography = styled(Typography)(({ theme }) => ({
fontSize: '2.5rem',
Expand Down
151 changes: 151 additions & 0 deletions src/components/Reports/PdsGoalCalculator/Salary/SalarySection.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { DesignationSupportSalaryType } from 'src/graphql/types.generated';
import {
PdsGoalCalculationMock,
PdsGoalCalculatorTestWrapper,
} from '../PdsGoalCalculatorTestWrapper';
import { SalarySection } from './SalarySection';

interface Props {
calculationMock?: PdsGoalCalculationMock;
}

const TestComponent: React.FC<Props> = ({ calculationMock }) => (
<PdsGoalCalculatorTestWrapper calculationMock={calculationMock}>
<SalarySection />
</PdsGoalCalculatorTestWrapper>
);

describe('SalarySection', () => {
it('renders the Salary heading', async () => {
const { findByRole } = render(
<TestComponent
calculationMock={{
salaryOrHourly: DesignationSupportSalaryType.Salaried,
// Yearly salary — divided by 12 for monthly base
payRate: 60000,
hoursWorkedPerWeek: null,
geographicLocation: null,
}}
/>,
);

expect(await findByRole('heading', { name: 'Salary' })).toBeInTheDocument();
});

describe('salaried breakdown', () => {
it('renders a six-row breakdown for a salaried user with no geographic multiplier', async () => {
const { findByTestId, getByTestId, getByRole } = render(
<TestComponent
calculationMock={{
salaryOrHourly: DesignationSupportSalaryType.Salaried,
// Yearly salary — divided by 12 for monthly base
payRate: 60000,
hoursWorkedPerWeek: null,
geographicLocation: null,
}}
/>,
);

await findByTestId('gross-monthly-pay');

// 60000 / 12 = 5000 monthly base
const monthlyBaseRow = getByRole('gridcell', {
name: /Monthly Base.*Yearly Salary ÷ 12/,
}).closest('[role="row"]');
expect(monthlyBaseRow).toHaveTextContent('$5,000');

expect(getByTestId('gross-monthly-pay')).toHaveTextContent('$5,000');
expect(getByTestId('employer-fica')).toHaveTextContent('$400');
expect(getByTestId('salary-subtotal')).toHaveTextContent('$5,400');
});

it('applies the geographic multiplier additively and surfaces the percent in the breakdown', async () => {
const { findByTestId, getByTestId, getByRole } = render(
<TestComponent
calculationMock={{
salaryOrHourly: DesignationSupportSalaryType.Salaried,
// Yearly salary — divided by 12 for monthly base
payRate: 60000,
hoursWorkedPerWeek: null,
// Orlando seeds at 0.06
geographicLocation: 'Orlando, FL',
}}
/>,
);

await findByTestId('gross-monthly-pay');

const geoRow = getByRole('gridcell', {
name: 'Geographic Multiplier',
}).closest('[role="row"]');
expect(geoRow).toHaveTextContent('6%');

// (60000 / 12) * (1 + 0.06) = 5300
expect(getByTestId('gross-monthly-pay')).toHaveTextContent('$5,300');
// 5300 * 0.08
expect(getByTestId('employer-fica')).toHaveTextContent('$424');
// 5300 + 424
expect(getByTestId('salary-subtotal')).toHaveTextContent('$5,724');
});
});

describe('hourly breakdown', () => {
it('renders Hours per Week and Monthly Base rows and uses the hourly formula', async () => {
const { findByTestId, getByTestId, getByRole } = render(
<TestComponent
calculationMock={{
salaryOrHourly: DesignationSupportSalaryType.Hourly,
payRate: 25,
hoursWorkedPerWeek: 40,
geographicLocation: null,
}}
/>,
);

await findByTestId('gross-monthly-pay');

const hoursRow = getByRole('gridcell', {
name: 'Hours per Week',
}).closest('[role="row"]');
expect(hoursRow).toHaveTextContent('40');

const monthlyBaseRow = getByRole('gridcell', {
name: /Monthly Base.*Pay Rate × Hours per Week/,
}).closest('[role="row"]');
// 25 * 40 * 52 / 12 = 4333.33
expect(monthlyBaseRow).toHaveTextContent('$4,333.33');

expect(getByTestId('gross-monthly-pay')).toHaveTextContent('$4,333.33');
// 4333.33 * 0.08 = 346.67
expect(getByTestId('employer-fica')).toHaveTextContent('$346.67');
// 4333.33 + 346.67 = 4680.00
expect(getByTestId('salary-subtotal')).toHaveTextContent('$4,680');
});
});

it('renders nothing when the EMPLOYER_FICA_RATE constant is missing', async () => {
const { container, queryByRole } = render(
<PdsGoalCalculatorTestWrapper
calculationMock={{
salaryOrHourly: DesignationSupportSalaryType.Salaried,
// Yearly salary — divided by 12 for monthly base
payRate: 60000,
hoursWorkedPerWeek: null,
geographicLocation: null,
}}
constantsMock={{ mpdGoalMiscConstants: [] }}
>
<SalarySection />
</PdsGoalCalculatorTestWrapper>,
);

await waitFor(() =>
expect(
queryByRole('heading', { name: 'Salary' }),
).not.toBeInTheDocument(),
);
expect(container).toBeEmptyDOMElement();
});
});
103 changes: 103 additions & 0 deletions src/components/Reports/PdsGoalCalculator/Salary/SalarySection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, { useMemo } from 'react';
import { Box, Typography, styled } from '@mui/material';
import { DataGrid } from '@mui/x-data-grid';
import { useTranslation } from 'react-i18next';
import { useGoalCalculatorConstants } from 'src/hooks/useGoalCalculatorConstants';
import { useLocale } from 'src/hooks/useLocale';
import { useDataGridLocaleText } from 'src/hooks/useMuiLocaleText';
import { usePdsGoalCalculator } from '../Shared/PdsGoalCalculatorContext';
import {
buildSalaryBreakdownColumns,
buildSalaryBreakdownRows,
} from './salaryBreakdown';

const GridContainer = styled(Box)({
display: 'flex',
flexDirection: 'column',
});

const StyledGrid = styled(DataGrid)(({ theme }) => ({
fontSize: theme.typography.body1.fontSize,
'.MuiDataGrid-columnHeaderTitle': {
fontWeight: 'bold',
color: theme.palette.mpdxBlue.main,
},
'.MuiDataGrid-row.top-border .MuiDataGrid-cell': {
borderTop: `2px solid ${theme.palette.divider}`,
},
'.MuiDataGrid-row[data-id="total"]': {
fontWeight: 'bold',
},
'.category-cell': {
lineHeight: 1.3,
paddingTop: theme.spacing(1),
paddingBottom: theme.spacing(1),
},
'.category-formula': {
display: 'block',
color: theme.palette.text.secondary,
fontSize: theme.typography.body2.fontSize,
},
})) as typeof DataGrid;

export const SalarySection: React.FC = () => {
const { t } = useTranslation();
const locale = useLocale();
const localeText = useDataGridLocaleText();
const { calculation } = usePdsGoalCalculator();
const { goalMiscConstants, goalGeographicConstantMap } =
useGoalCalculatorConstants();

const employerFicaRate =
goalMiscConstants.ADDITIONAL_RATES?.EMPLOYER_FICA_RATE?.fee;

const rows = useMemo(() => {
if (!calculation || employerFicaRate === undefined) {
return [];
}
const { geographicLocation } = calculation;
const geographicMultiplier =
goalGeographicConstantMap.get(geographicLocation ?? '') ?? 0;

return buildSalaryBreakdownRows(
calculation,
{ geographicMultiplier, employerFicaRate },
locale,
t,
);
}, [calculation, employerFicaRate, goalGeographicConstantMap, locale, t]);

const columns = useMemo(
() => buildSalaryBreakdownColumns(locale, t),
[locale, t],
);

if (rows.length === 0) {
return null;
}

return (
<>
<Typography variant="h6" pb={2}>
{t('Salary')}
</Typography>
<GridContainer>
<StyledGrid
aria-label={t('Salary breakdown')}
rows={rows}
columns={columns}
getRowClassName={(params) =>
params.id === 'gross-monthly-pay' || params.id === 'total'
? 'top-border'
: ''
}
disableColumnMenu
disableColumnSorting
disableRowSelectionOnClick
hideFooter
localeText={localeText}
/>
</GridContainer>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { PdsGoalCalculatorTestWrapper } from '../PdsGoalCalculatorTestWrapper';
import { SalaryStep } from './SalaryStep';

describe('SalaryStep', () => {
it('renders the support item heading', () => {
const { getByText } = render(
it('renders the Salary section', async () => {
const { findByRole } = render(
<PdsGoalCalculatorTestWrapper>
<SalaryStep />
</PdsGoalCalculatorTestWrapper>,
);

expect(getByText('Support Item')).toBeInTheDocument();
expect(await findByRole('heading', { name: 'Salary' })).toBeInTheDocument();
});
});
11 changes: 2 additions & 9 deletions src/components/Reports/PdsGoalCalculator/Salary/SalaryStep.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import React from 'react';
import { Typography } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { SalarySection } from './SalarySection';

export const SalaryStep: React.FC = () => {
const { t } = useTranslation();

return (
<Typography variant="h5" p={3}>
{t('Support Item')}
</Typography>
);
return <SalarySection />;
};
Loading
Loading