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 @@ -6,6 +6,20 @@ import { PdsGoalCalculatorTestWrapper } from '../PdsGoalCalculatorTestWrapper';

describe('PdsGoalCard', () => {
it('renders the calculated PDS goal amount', async () => {
// Hand-derivation of $8,073.02 from the pinned defaults in
// PdsGoalCalculatorTestWrapper (payRate=50000 salaried full-time, benefits=1500,
// no geographic multiplier, all reimbursable fields=0, 403b=0):
// monthlyBase = 50000 / 12 = 4166.67
// grossMonthlyPay = monthlyBase * 1 = 4166.67 (geo multiplier 0)
// employerFica = 4166.67 * 0.08 = 333.33
// salarySubtotal = 4500
// reimbursable = max(300, 0) = 300 (REIMBURSABLE_FLOOR)
// subtotal = 4500 + 300 + 0 (403b) + 0 (workComp) + 1500 (benefits) = 6300
// attrition = 6300 * 0.06 = 378
// creditCardFees = 6678 / 0.94 - 6678 ≈ 426.26
// adminBase = 6300 + 378 + 426.26 ≈ 7104.26
// assessment = 7104.26 / 0.88 - 7104.26 ≈ 968.76
// overallTotal = 6300 + 378 + 426.26 + 968.76 ≈ 8073.02
const { findByText } = render(
<PdsGoalCalculatorTestWrapper
withProvider={false}
Expand All @@ -17,7 +31,7 @@ describe('PdsGoalCard', () => {
</PdsGoalCalculatorTestWrapper>,
);

expect(await findByText('$849.44')).toBeInTheDocument();
expect(await findByText('$8,073.02')).toBeInTheDocument();
Comment thread
wjames111 marked this conversation as resolved.
});

it('builds the View link with the PDS goal calculator path', async () => {
Expand Down
29 changes: 8 additions & 21 deletions src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
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';
import { DesignationSupportFormType } from 'src/graphql/types.generated';
import { useAccountListId } from 'src/hooks/useAccountListId';
import { useGoalCalculatorConstants } from 'src/hooks/useGoalCalculatorConstants';
import {
PdsGoalCalculationFieldsFragment,
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 +20,14 @@ export const PdsGoalCard: React.FC<PdsGoalCardProps> = ({ goal }) => {
const accountListId = useAccountListId() ?? '';
const [deletePdsGoalCalculation] = useDeletePdsGoalCalculationMutation();

const {
goalMiscConstants,
goalGeographicConstantMap,
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 { data: summaryData, loading: summaryLoading } = usePdsSummaryData(
goal,
hcmUser,
);
const goalTotal = summaryData?.overallTotal ?? 0;
Comment thread
wjames111 marked this conversation as resolved.

const formType = goal.formType ?? DesignationSupportFormType.Detailed;
const formTypeBadge =
Expand All @@ -65,7 +52,7 @@ export const PdsGoalCard: React.FC<PdsGoalCardProps> = ({ goal }) => {
name={goal.name}
goalAmount={goalTotal}
currency="USD"
loading={constantsLoading || hcmLoading}
loading={summaryLoading || hcmLoading}
updatedAt={goal.updatedAt}
viewHref={`/accountLists/${accountListId}/hrTools/pdsGoalCalculator/${goal.id}`}
onDelete={handleDelete}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,19 @@ const calculationsDefault = gqlMock<
salaryOrHourly: DesignationSupportSalaryType.Salaried,
payRate: 50000,
hoursWorkedPerWeek: null,
averageHoursPerWeek: 0,
benefits: 1500,
geographicLocation: null,
ministryCellPhone: 0,
ministryInternet: 0,
mpdNewsletter: 0,
mpdMiscellaneous: 0,
accountTransfers: 0,
otherMonthlyReimbursements: 0,
conferenceRetreatCosts: 0,
ministryTravelMeals: 0,
otherAnnualReimbursements: 0,
designationSupportHoursItems: [],
},
],
pageInfo: {
Expand Down Expand Up @@ -80,8 +91,19 @@ const calculationDefault = gqlMock<
salaryOrHourly: DesignationSupportSalaryType.Salaried,
payRate: 50000,
hoursWorkedPerWeek: null,
averageHoursPerWeek: 0,
benefits: 1500,
geographicLocation: null,
ministryCellPhone: 0,
ministryInternet: 0,
mpdNewsletter: 0,
mpdMiscellaneous: 0,
accountTransfers: 0,
otherMonthlyReimbursements: 0,
conferenceRetreatCosts: 0,
ministryTravelMeals: 0,
otherAnnualReimbursements: 0,
designationSupportHoursItems: [],
},
},
variables: { id: 'goal-1' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const PdsGoalCalculatorProvider: React.FC<Props> = ({ children }) => {
const { data: hcmData } = useHcmUserQuery();
const hcmUser = hcmData?.hcm[0];

const summaryData = usePdsSummaryData(calculation, hcmUser);
const { data: summaryData } = usePdsSummaryData(calculation, hcmUser);

// Track the user's place by step enum, not numeric index, so that a change
// to the steps array (e.g. formType switch Detailed → Simple, dropping the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,19 +116,25 @@
{
id: 'credit-card-fees',
category: t('Credit Card Fees'),
formula: t('(Subtotal + Attrition) × {{rate}}', {
rate: percentageFormat(constants.creditCardFeeRate, locale),
}),
formula: t(
'(Subtotal + Attrition) ÷ (1 − {{rate}}) − (Subtotal + Attrition)',
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

**[High] Long gross-up formula labels will clip in the data grid.** — sev 8.0/10

Flagged by: UX (8.0), Architecture (rendering-contract confirmation).

The new formula here grows from ~30 chars to ~58 chars, and the Assessment formula at line 133 reaches ~100 chars. The category cell at styledGrid.tsx:27-36 stacks the category label over the formula in a flex column. MUI X DataGrid defaults to rowHeight={52} and neither SalarySection.tsx:47 nor OtherSection.tsx:48 (both non-PR files) override it with getRowHeight={() => 'auto'}. The longest formula will wrap to 2-3 lines on viewports < ~1100px and on tablet/mobile, then clip at the third line.

Cheapest fix (one line per non-PR container file):

// in SalarySection.tsx and OtherSection.tsx
<StyledGrid
  // ...existing props
  getRowHeight={() => 'auto'}
  // ...
/>

Alternative: shorten the in-PR formula labels — see Dismissed Findings F6 in the PR body (author has already pushed back on rewording for this PR scope; the container-level fix is the lower-friction path).

{
rate: percentageFormat(constants.creditCardFeeRate, locale),
},
),
amount: totals.creditCardFees,
testId: 'other-credit-card-fees',
bold: true,
},
{
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) ÷ (1 − {{rate}}) − (Subtotal + Attrition + Credit Card Fees)',
Comment thread
wjames111 marked this conversation as resolved.
{
rate: percentageFormat(constants.adminRate, locale),
},
),

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.
Comment thread
wjames111 marked this conversation as resolved.
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}}', {
Comment thread
wjames111 marked this conversation as resolved.
rate: percentageFormat(1 + geographicMultiplier, locale),
Comment thread
wjames111 marked this conversation as resolved.
}),
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,26 @@ 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
Comment thread
wjames111 marked this conversation as resolved.
expect(result.creditCardFees).toBeCloseTo(500.68);
});

it('returns 0 when creditCardFeeRate is 0', () => {
const result = calculateOtherExpenses(fullTime(), {
...defaultConstants,
creditCardFeeRate: 0,
});
expect(result.creditCardFees).toBe(0);
});
});

describe('assessment', () => {
it('is (subtotal + creditCardFees + attrition) × adminRate', () => {
it('grosses up (subtotal + attrition + creditCardFees) 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+444+500.68=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 +177,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 + attrition + creditCardFees;
// 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;
Comment thread
wjames111 marked this conversation as resolved.
Comment thread
wjames111 marked this conversation as resolved.

return {
reimbursableExpenses,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
GoalGeographicConstantMap,
GoalMiscConstants,
} from 'src/hooks/useGoalCalculatorConstants';
import { buildPdsGoalConstants } from './calculatePdsGoalTotal';
import { buildPdsGoalConstants } from './pdsGoalConstants';

const makeConstant = (fee: number) => ({
fee,
Expand Down Expand Up @@ -108,7 +108,7 @@ describe('buildPdsGoalConstants', () => {
expect(result).toBeNull();
});

it('returns geographicMultiplier of 0 for unknown location', () => {
it('defaults geographicMultiplier to 0 (no adjustment) for unknown location', () => {
const result = buildPdsGoalConstants(
buildMiscConstants(),
defaultGeoMap,
Expand All @@ -119,7 +119,7 @@ describe('buildPdsGoalConstants', () => {
expect(result?.geographicMultiplier).toBe(0);
});

it('returns geographicMultiplier of 0 when location is null', () => {
it('defaults geographicMultiplier to 0 (no adjustment) when location is null', () => {
const result = buildPdsGoalConstants(
buildMiscConstants(),
defaultGeoMap,
Expand All @@ -130,7 +130,7 @@ describe('buildPdsGoalConstants', () => {
expect(result?.geographicMultiplier).toBe(0);
});

it('returns geographicMultiplier of 0 when location is undefined', () => {
it('defaults geographicMultiplier to 0 (no adjustment) when location is undefined', () => {
const result = buildPdsGoalConstants(
buildMiscConstants(),
defaultGeoMap,
Expand Down
Loading
Loading