Skip to content
Open
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 @@ -17,7 +17,7 @@ describe('PdsGoalCard', () => {
</PdsGoalCalculatorTestWrapper>,
);

expect(await findByText('$849.44')).toBeInTheDocument();
expect(await findByText('$8,073.02')).toBeInTheDocument();
});

it('builds the View link with the PDS goal calculator path', async () => {
Expand Down
24 changes: 5 additions & 19 deletions src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React from 'react';
import { Chip } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { GoalCard } from 'src/components/Reports/Shared/GoalCard/GoalCard';
Expand All @@ -10,10 +10,7 @@ import {
useDeletePdsGoalCalculationMutation,
} from '../GoalsList/PdsGoalCalculations.generated';
import { useHcmUserQuery } from '../Shared/HCM.generated';
import {
buildPdsGoalConstants,
calculatePdsGoalTotal,
} from '../calculations/calculatePdsGoalTotal';
import { usePdsSummaryData } from '../calculations/usePdsSummaryData';

export interface PdsGoalCardProps {
goal: PdsGoalCalculationFieldsFragment;
Expand All @@ -24,23 +21,12 @@ export const PdsGoalCard: React.FC<PdsGoalCardProps> = ({ goal }) => {
const accountListId = useAccountListId() ?? '';
const [deletePdsGoalCalculation] = useDeletePdsGoalCalculationMutation();

const {
goalMiscConstants,
goalGeographicConstantMap,
loading: constantsLoading,
} = useGoalCalculatorConstants();
const { loading: constantsLoading } = useGoalCalculatorConstants();
const { data: hcmData, loading: hcmLoading } = useHcmUserQuery();
const hcmUser = hcmData?.hcm[0];

const goalTotal = useMemo(() => {
const constants = buildPdsGoalConstants(
goalMiscConstants,
goalGeographicConstantMap,
goal.geographicLocation,
hcmUser?.fourOThreeB,
);
return constants ? calculatePdsGoalTotal(goal, constants) : 0;
}, [goal, goalMiscConstants, goalGeographicConstantMap, hcmUser]);
const summaryData = usePdsSummaryData(goal, hcmUser);
const goalTotal = summaryData?.overallTotal ?? 0;

const formType = goal.formType ?? DesignationSupportFormType.Detailed;
const formTypeBadge =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,15 +194,15 @@ export const PdsGoalCalculatorTestWrapper: React.FC<
mpdGoalGeographicConstants: [
{
location: 'None',
percentageMultiplier: 0,
percentageMultiplier: 1,
},
{
location: 'Orlando, FL',
percentageMultiplier: 0.06,
percentageMultiplier: 1.06,
},
{
location: 'New York, NY',
percentageMultiplier: 0.12,
percentageMultiplier: 1.12,
},
],
mpdGoalMiscConstants: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ describe('AnnualReimbursableSection', () => {
expect(getAllByRole('row')).toHaveLength(5);
});

it('renders the info tooltip icon with an accessible label', async () => {
const { findByLabelText } = render(<TestComponent />);
it('renders the subtitle describing the annual calculation', async () => {
const { findByText } = render(<TestComponent />);

expect(
await findByLabelText(
await findByText(
'This annual amount will be divided by 12 when added to the total.',
),
).toBeInTheDocument();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const AnnualReimbursableSection: React.FC = () => {
return (
<ReimbursableExpensesGrid
title={t('Annual Reimbursable Expenses')}
titleTooltip={t(
subTitle={t(
'This annual amount will be divided by 12 when added to the total.',
)}
fields={fields}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ const fields: ReimbursableField[] = [

interface TestComponentProps {
subtotalValue?: number;
subTitle?: string;
}

const TestComponent: React.FC<TestComponentProps> = ({ subtotalValue = 0 }) => (
const TestComponent: React.FC<TestComponentProps> = ({
subtotalValue = 0,
subTitle,
}) => (
<PdsGoalCalculatorTestWrapper
onCall={mutationSpy}
calculationMock={{
Expand All @@ -33,6 +37,7 @@ const TestComponent: React.FC<TestComponentProps> = ({ subtotalValue = 0 }) => (
>
<ReimbursableExpensesGrid
title="Test Section"
subTitle={subTitle}
fields={fields}
subtotalLabel="Test Subtotal"
subtotalValue={subtotalValue}
Expand Down Expand Up @@ -66,6 +71,14 @@ describe('ReimbursableExpensesGrid', () => {
expect(getAllByRole('row')).toHaveLength(4);
});

it('renders the subtitle when provided', async () => {
const { findByText } = render(
<TestComponent subTitle="Test subtitle text" />,
);

expect(await findByText('Test subtitle text')).toBeInTheDocument();
});

it('clears the error after a subsequent valid edit', async () => {
const { findByRole, queryByRole } = render(<TestComponent />);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import React, { useState } from 'react';
import InfoIcon from '@mui/icons-material/Info';
import {
Box,
Card,
FormHelperText,
Stack,
Tooltip,
Typography,
styled,
} from '@mui/material';
Expand Down Expand Up @@ -34,7 +32,7 @@ interface ReimbursableRow {

interface ReimbursableExpensesGridProps {
title: string;
titleTooltip?: string;
subTitle?: string;
fields: ReimbursableField[];
subtotalLabel: string;
subtotalValue: number;
Expand All @@ -58,7 +56,7 @@ export const ReimbursableExpensesGrid: React.FC<
ReimbursableExpensesGridProps
> = ({
title,
titleTooltip,
subTitle,
fields,
subtotalLabel,
subtotalValue,
Expand Down Expand Up @@ -162,13 +160,11 @@ export const ReimbursableExpensesGrid: React.FC<

return (
<>
<Stack direction="row" alignItems="center" gap={0.5} pb={2}>
<Stack direction="column" alignItems="left" gap={0.5} pb={2}>
<Typography variant="h6">{title}</Typography>
{titleTooltip && (
<Tooltip title={titleTooltip}>
<InfoIcon color="action" aria-label={titleTooltip} />
</Tooltip>
)}
<Typography variant="body2" color="textSecondary">
{subTitle}
</Typography>
</Stack>
<StyledCard>
<BaseGrid
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { PdsGoalCalculatorTestWrapper } from '../PdsGoalCalculatorTestWrapper';
import { TotalReimbursableSection } from './TotalReimbursableSection';

Expand Down Expand Up @@ -40,24 +39,16 @@ describe('TotalReimbursableSection', () => {
).toBeInTheDocument();
});

it('renders the info icon with an accessible label', async () => {
const { findByLabelText } = render(<TestComponent />);
it('renders the subtitle describing the $300 minimum floor', async () => {
const { findByText } = render(<TestComponent />);

expect(
await findByLabelText('Total reimbursable information'),
await findByText(
'The total is the greater of the $300 minimum or your calculated amount.',
),
).toBeInTheDocument();
});

it('shows a tooltip describing the $300 minimum floor on hover', async () => {
const { findByLabelText, findByRole } = render(<TestComponent />);

userEvent.hover(await findByLabelText('Total reimbursable information'));

expect(await findByRole('tooltip')).toHaveTextContent(
'The total is the greater of the $300 minimum or your calculated amount.',
);
});

it('applies the $300 floor when the calculated amount is below it', async () => {
const { getByTestId } = render(
<TestComponent monthlyCellPhone={100} annualConference={0} />,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import InfoIcon from '@mui/icons-material/Info';
import {
Card,
CardContent,
Stack,
Tooltip,
Typography,
styled,
} from '@mui/material';
import { Card, CardContent, Stack, Typography, styled } from '@mui/material';
import { useTranslation } from 'react-i18next';
import { useLocale } from 'src/hooks/useLocale';
import { currencyFormat } from 'src/lib/intlFormat';
Expand Down Expand Up @@ -36,23 +28,18 @@ export const TotalReimbursableSection: React.FC = () => {
return (
<Card>
<CardContent>
<Stack direction="row" alignItems="center" gap={0.5}>
<Stack direction="column" alignItems="left" gap={0.5}>
<Typography variant="h6">
{t('Total Reimbursable Expenses')}
</Typography>
<Tooltip
title={t(
<Typography variant="body2" color="textSecondary">
{t(
'The total is the greater of the {{floor}} minimum or your calculated amount.',
{
floor: currencyFormat(REIMBURSABLE_FLOOR, 'USD', locale),
},
)}
>
<InfoIcon
color="action"
aria-label={t('Total reimbursable information')}
/>
</Tooltip>
</Typography>
</Stack>
<AmountTypography data-testid="reimbursable-total">
{currencyFormat(total, 'USD', locale)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,15 @@
{
id: 'assessment',
category: t('Assessment'),
formula: t('(Subtotal + Credit Card Fees + Attrition) × {{rate}}', {
rate: percentageFormat(constants.adminRate, locale),
}),
formula: t(
'(Subtotal + Attrition + Credit Card Fees) ÷ {{divisor}} − (Subtotal + Attrition + Credit Card Fees)',
{
divisor: new Intl.NumberFormat(locale, {
minimumFractionDigits: 2,
maximumFractionDigits: 4,
}).format(1 - constants.adminRate),
},
),

Check warning on line 137 in src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

buildOtherBreakdownRows increases from 102 to 108 lines of code, threshold = 100. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
amount: totals.assessment,
testId: 'other-assessment',
bold: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ describe('buildSalaryBreakdownRows', () => {
expect(rows.map((row) => row.id)).toEqual([
'pay-rate',
'monthly-base',
'geographic-multiplier',
'gross-monthly-pay',
'employer-fica',
'total',
Expand All @@ -69,8 +68,6 @@ describe('buildSalaryBreakdownRows', () => {
expect(byId['pay-rate']).toBe(60000);
// monthlyBase = 60000 / 12 = 5000
expect(byId['monthly-base']).toBe(5000);
// geographicMultiplier passed through from constants
expect(byId['geographic-multiplier']).toBe(0);
// grossMonthlyPay = 5000 * (1 + 0) = 5000
expect(byId['gross-monthly-pay']).toBe(5000);
// employerFica = 5000 * 0.08 = 400
Expand Down Expand Up @@ -113,7 +110,6 @@ describe('buildSalaryBreakdownRows', () => {
'pay-rate',
'hours-per-week',
'monthly-base',
'geographic-multiplier',
'gross-monthly-pay',
'employer-fica',
'total',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,12 @@ export const buildSalaryBreakdownRows = (
amount: monthlyBase,
format: 'currency',
},
{
id: 'geographic-multiplier',
category: t('Geographic Multiplier'),
amount: geographicMultiplier,
format: 'percentage',
},
{
id: 'gross-monthly-pay',
category: t('Gross Monthly Pay'),
formula: t('Monthly Base × (1 + Geographic Multiplier)'),
formula: t('Monthly Base × {{rate}}', {
rate: percentageFormat(1 + geographicMultiplier, locale),
}),
amount: grossMonthlyPay,
format: 'currency',
testId: 'gross-monthly-pay',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,19 +137,18 @@ describe('calculateOtherExpenses', () => {
});

describe('credit card fees', () => {
it('is 6% of (subtotal + attrition)', () => {
it('grosses up (subtotal + attrition) so that fees are `creditCardFeeRate` of the post-fees total', () => {
const result = calculateOtherExpenses(fullTime(), defaultConstants);
// (7400 + 444) * 0.06
expect(result.creditCardFees).toBeCloseTo(470.64);
// (7400 + 444) / (1 - 0.06) - (7400 + 444) ≈ 500.68
expect(result.creditCardFees).toBeCloseTo(500.68);
});
});

describe('assessment', () => {
it('is (subtotal + creditCardFees + attrition) × adminRate', () => {
it('grosses up (subtotal + creditCardFees + attrition) so that admin is `adminRate` of the post-admin total', () => {
const result = calculateOtherExpenses(fullTime(), defaultConstants);
// subtotal=7400, attrition=444, creditCardFees=470.64
// (7400 + 470.64 + 444) * 0.12 ≈ 997.76
expect(result.assessment).toBeCloseTo(997.76, 1);
// adminBase=7400+500.68+444=8344.68; assessment = adminBase/0.88 - adminBase ≈ 1137.91
expect(result.assessment).toBeCloseTo(1137.91, 1);
});

it('returns 0 when adminRate is 0', () => {
Expand All @@ -170,21 +169,22 @@ describe('calculateOtherExpenses', () => {
expect(result.benefits).toBe(1500);
expect(result.subtotal).toBeCloseTo(7400);
expect(result.attrition).toBeCloseTo(444);
expect(result.creditCardFees).toBeCloseTo(470.64);
expect(result.assessment).toBeCloseTo(997.76, 1);
expect(result.creditCardFees).toBeCloseTo(500.68);
expect(result.assessment).toBeCloseTo(1137.91, 1);
});

it('produces correct totals for a part-time employee', () => {
const result = calculateOtherExpenses(partTime(), defaultConstants);
// reimbursable=500, 403b=400, workComp=4000*0.17=680, benefits=0
// subtotal=5000+500+400+680+0=6580
// attrition=6580*0.06=394.80
// creditCardFees=(6580+394.80)*0.06=418.49
// assessment=(6580+418.49+394.80)*0.12≈887.19
// creditCardFees=(6580+394.80)/(1-0.06)-(6580+394.80)≈445.20
// adminBase=6580+445.20+394.80=7420
// assessment = adminBase/0.88 - adminBase ≈ 1011.82
expect(result.subtotal).toBeCloseTo(6580);
expect(result.attrition).toBeCloseTo(394.8);
expect(result.creditCardFees).toBeCloseTo(418.49, 1);
expect(result.assessment).toBeCloseTo(887.19, 1);
expect(result.creditCardFees).toBeCloseTo(445.2, 1);
expect(result.assessment).toBeCloseTo(1011.82, 1);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,13 @@ export const calculateOtherExpenses = (
benefits;

const attrition = subtotal * constants.attritionRate;
const creditCardFees = (subtotal + attrition) * constants.creditCardFeeRate;
const adminRate = constants.adminRate;
const assessment = (subtotal + creditCardFees + attrition) * adminRate;
const creditCardFees =
(subtotal + attrition) / (1 - constants.creditCardFeeRate) -
(subtotal + attrition);
const adminBase = subtotal + creditCardFees + attrition;
// Admin assessment is `adminRate` of the post-admin total, not a markup on
// `adminBase`, so gross up: assessment / (adminBase + assessment) = adminRate.
const assessment = adminBase / (1 - constants.adminRate) - adminBase;

return {
reimbursableExpenses,
Expand Down
Loading
Loading