From 6c5b159eafbfe9c03e82b33d3efc9a27be94a11f Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 13 May 2026 15:37:01 -0400 Subject: [PATCH 01/10] Remove +1 geographic multiplier --- .../HrTools/PdsGoalCalculator/calculations/salaryCalculation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.ts b/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.ts index d47af4622d..9be7972240 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.ts @@ -31,7 +31,7 @@ export const calculateSalaryTotals = ( calculation.salaryOrHourly === DesignationSupportSalaryType.Salaried; const monthlyBase = isSalaried ? payRate / 12 : (payRate * hours * 52) / 12; - const grossMonthlyPay = monthlyBase * (1 + geographicMultiplier); + const grossMonthlyPay = monthlyBase * geographicMultiplier; const employerFica = grossMonthlyPay * employerFicaRate; const subtotal = grossMonthlyPay + employerFica; From a7cc3b4e0319ad521324e3313e3e73d066960d4f Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 13 May 2026 15:48:58 -0400 Subject: [PATCH 02/10] Fix goal total bug --- .../GoalCard/PdsGoalCard.test.tsx | 2 +- .../GoalCard/PdsGoalCard.tsx | 24 +--- .../buildPdsGoalConstants.test.ts | 2 +- .../calculatePdsGoalTotal.test.ts | 134 ------------------ ...atePdsGoalTotal.ts => pdsGoalConstants.ts} | 43 +----- .../calculations/usePdsSummaryData.test.ts | 33 ----- .../calculations/usePdsSummaryData.ts | 2 +- 7 files changed, 10 insertions(+), 230 deletions(-) delete mode 100644 src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts rename src/components/HrTools/PdsGoalCalculator/calculations/{calculatePdsGoalTotal.ts => pdsGoalConstants.ts} (70%) diff --git a/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx b/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx index 1f508b8339..ea14e11621 100644 --- a/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx @@ -17,7 +17,7 @@ describe('PdsGoalCard', () => { , ); - expect(await findByText('$849.44')).toBeInTheDocument(); + expect(await findByText('$2,265.18')).toBeInTheDocument(); }); it('builds the View link with the PDS goal calculator path', async () => { diff --git a/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.tsx b/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.tsx index 70c2b32d80..14f74e1b03 100644 --- a/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.tsx +++ b/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.tsx @@ -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'; @@ -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; @@ -24,23 +21,12 @@ export const PdsGoalCard: React.FC = ({ 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 = diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/buildPdsGoalConstants.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/buildPdsGoalConstants.test.ts index 1a398f7b1e..a9b8b71729 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/buildPdsGoalConstants.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/buildPdsGoalConstants.test.ts @@ -2,7 +2,7 @@ import { GoalGeographicConstantMap, GoalMiscConstants, } from 'src/hooks/useGoalCalculatorConstants'; -import { buildPdsGoalConstants } from './calculatePdsGoalTotal'; +import { buildPdsGoalConstants } from './pdsGoalConstants'; const makeConstant = (fee: number) => ({ fee, diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts deleted file mode 100644 index d5c613222a..0000000000 --- a/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { - DesignationSupportFormType, - DesignationSupportSalaryType, - DesignationSupportStatus, -} from 'src/graphql/types.generated'; -import { - PdsGoalTotalConstants, - PdsGoalTotalFields, - calculatePdsGoalTotal, -} from './calculatePdsGoalTotal'; - -const defaultConstants: PdsGoalTotalConstants = { - employerFicaRate: 0.08, - workCompPercentage: 0.17, - attritionRate: 0.06, - creditCardFeeRate: 0.06, - adminRate: 0.12, - fourOThreeBPercentage: 0.1, - geographicMultiplier: 0, -}; - -const makeGoal = ( - overrides: Partial = {}, -): PdsGoalTotalFields => ({ - hoursWorkedPerWeek: null, - salaryOrHourly: DesignationSupportSalaryType.Salaried, - status: DesignationSupportStatus.FullTime, - payRate: 60000, - benefits: 1500, - geographicLocation: null, - ministryCellPhone: 50, - ministryInternet: 50, - mpdNewsletter: 50, - mpdMiscellaneous: 50, - accountTransfers: 50, - otherMonthlyReimbursements: 50, - conferenceRetreatCosts: 0, - ministryTravelMeals: 0, - otherAnnualReimbursements: 0, - ...overrides, -}); - -describe('calculatePdsGoalTotal', () => { - it('computes the final assessment for a full-time salaried employee', () => { - const goal = makeGoal(); - const result = calculatePdsGoalTotal(goal, defaultConstants); - // assessment ≈ 1038.21 - expect(result).toBeCloseTo(1038.21, 1); - }); - - it('computes the final assessment for a part-time hourly employee', () => { - const goal = makeGoal({ - status: DesignationSupportStatus.PartTime, - salaryOrHourly: DesignationSupportSalaryType.Hourly, - payRate: 25, - hoursWorkedPerWeek: 20, - benefits: null, - }); - const result = calculatePdsGoalTotal(goal, defaultConstants); - expect(result).toBeCloseTo(434.83, 0); - }); - - it('returns a positive value when payRate is null', () => { - const goal = makeGoal({ payRate: null }); - const result = calculatePdsGoalTotal(goal, defaultConstants); - expect(result).toBeGreaterThan(0); - }); - - it('applies geographic multiplier', () => { - const goal = makeGoal(); - const withGeo = calculatePdsGoalTotal(goal, { - ...defaultConstants, - geographicMultiplier: 0.06, - }); - const withoutGeo = calculatePdsGoalTotal(goal, defaultConstants); - expect(withGeo).toBeGreaterThan(withoutGeo); - }); - - it('excludes reimbursable expenses and 403b when formType is Simple', () => { - const baseline = calculatePdsGoalTotal( - makeGoal({ formType: DesignationSupportFormType.Simple }), - { ...defaultConstants, fourOThreeBPercentage: 0.1 }, - ); - - const withDifferentReimbursables = calculatePdsGoalTotal( - makeGoal({ - formType: DesignationSupportFormType.Simple, - ministryCellPhone: 9999, - otherAnnualReimbursements: 9999, - }), - { ...defaultConstants, fourOThreeBPercentage: 0.1 }, - ); - expect(withDifferentReimbursables).toBeCloseTo(baseline); - - const withDifferent403b = calculatePdsGoalTotal( - makeGoal({ formType: DesignationSupportFormType.Simple }), - { ...defaultConstants, fourOThreeBPercentage: 0.5 }, - ); - expect(withDifferent403b).toBeCloseTo(baseline); - }); - - it('includes reimbursable expenses and 403b when formType is Detailed', () => { - const baseline = calculatePdsGoalTotal( - makeGoal({ formType: DesignationSupportFormType.Detailed }), - { ...defaultConstants, fourOThreeBPercentage: 0.1 }, - ); - - const withDifferentReimbursables = calculatePdsGoalTotal( - makeGoal({ - formType: DesignationSupportFormType.Detailed, - ministryCellPhone: 9999, - otherAnnualReimbursements: 9999, - }), - { ...defaultConstants, fourOThreeBPercentage: 0.1 }, - ); - expect(withDifferentReimbursables).toBeGreaterThan(baseline); - - const withDifferent403b = calculatePdsGoalTotal( - makeGoal({ formType: DesignationSupportFormType.Detailed }), - { ...defaultConstants, fourOThreeBPercentage: 0.5 }, - ); - expect(withDifferent403b).toBeGreaterThan(baseline); - }); - - it('treats null formType the same as Detailed (legacy goals)', () => { - const legacy = makeGoal({ formType: null }); - const detailed = makeGoal({ - formType: DesignationSupportFormType.Detailed, - }); - expect(calculatePdsGoalTotal(legacy, defaultConstants)).toBeCloseTo( - calculatePdsGoalTotal(detailed, defaultConstants), - ); - }); -}); diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.ts b/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts similarity index 70% rename from src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.ts rename to src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts index 36e9aad328..83632dea10 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/calculatePdsGoalTotal.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts @@ -4,24 +4,8 @@ import { GoalMiscConstants, } from 'src/hooks/useGoalCalculatorConstants'; import { HcmUserQuery } from '../Shared/HCM.generated'; -import { - OtherExpensesConstants, - OtherExpensesFields, - calculateOtherExpenses, -} from './OtherExpenses'; -import { - ReimbursableCalculationFields, - calculateReimbursableTotals, -} from './reimbursableExpenses'; -import { - SalaryCalculationFields, - SalaryTotals, - calculateSalaryTotals, -} from './salaryCalculation'; - -export type PdsGoalTotalFields = SalaryCalculationFields & - ReimbursableCalculationFields & - OtherExpensesFields; +import { OtherExpensesConstants } from './OtherExpenses'; +import { SalaryTotals } from './salaryCalculation'; export interface PdsGoalTotalConstants { employerFicaRate: number; @@ -97,26 +81,3 @@ export const buildOtherExpensesConstants = ( }; }; -export const calculatePdsGoalTotal = ( - calculation: PdsGoalTotalFields, - constants: PdsGoalTotalConstants, -): number => { - const salaryTotals = calculateSalaryTotals(calculation, { - geographicMultiplier: constants.geographicMultiplier, - employerFicaRate: constants.employerFicaRate, - }); - - const reimbursableTotal = calculateReimbursableTotals(calculation).total; - - const otherExpenses = calculateOtherExpenses( - calculation, - buildOtherExpensesConstants( - calculation.formType ?? DesignationSupportFormType.Detailed, - constants, - salaryTotals, - reimbursableTotal, - ), - ); - - return otherExpenses.assessment; -}; diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts index 9e58409a7c..2f93b9a08c 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts @@ -17,10 +17,6 @@ import { PdsGoalCalculationFieldsFragmentDoc, } from '../GoalsList/PdsGoalCalculations.generated'; import { HcmUserDocument, HcmUserQuery } from '../Shared/HCM.generated'; -import { - PdsGoalTotalConstants, - calculatePdsGoalTotal, -} from './calculatePdsGoalTotal'; import { usePdsSummaryData } from './usePdsSummaryData'; jest.mock('src/hooks/useGoalCalculatorConstants'); @@ -389,33 +385,4 @@ describe('usePdsSummaryData', () => { }); }); - describe('consistency with calculatePdsGoalTotal', () => { - // Mirrors what buildPdsGoalConstants would derive from the mocked - // useGoalCalculatorConstants + defaultHcmUser, so we can call - // calculatePdsGoalTotal directly without the hook. - const directConstants: PdsGoalTotalConstants = { - employerFicaRate: EMPLOYER_FICA_RATE, - workCompPercentage: WORK_COMP_PERCENTAGE, - attritionRate: ATTRITION_RATE, - creditCardFeeRate: CREDIT_CARD_FEE_RATE, - adminRate: ADMIN_RATE, - fourOThreeBPercentage: 0.08, - geographicMultiplier: 0, - }; - - it.each([ - DesignationSupportFormType.Detailed, - DesignationSupportFormType.Simple, - ])( - 'calculatePdsGoalTotal matches usePdsSummaryData.otherTotals.assessment when formType is %s', - (formType) => { - const calc = { ...defaultCalculation, formType }; - const { result } = renderHook(() => - usePdsSummaryData(calc, defaultHcmUser), - ); - const direct = calculatePdsGoalTotal(calc, directConstants); - expect(direct).toBeCloseTo(result.current!.otherTotals.assessment, 5); - }, - ); - }); }); diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.ts b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.ts index 0dc642c228..9e4ff50f8b 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.ts @@ -11,7 +11,7 @@ import { import { buildOtherExpensesConstants, buildPdsGoalConstants, -} from './calculatePdsGoalTotal'; +} from './pdsGoalConstants'; import { ReimbursableTotals, calculateReimbursableTotals, From 43c10594d22e9ac269fb586e83916262800705f4 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 13 May 2026 16:34:38 -0400 Subject: [PATCH 03/10] Maybe fix attrition rate, unclear --- .../GoalCard/PdsGoalCard.test.tsx | 2 +- .../SupportItem/otherBreakdown.tsx | 12 +++++++++--- .../calculations/OtherExpenses.test.ts | 14 +++++++------- .../calculations/OtherExpenses.ts | 6 ++++-- .../calculations/pdsGoalConstants.ts | 1 - .../calculations/usePdsSummaryData.test.ts | 1 - 6 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx b/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx index ea14e11621..5385552eb0 100644 --- a/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx @@ -17,7 +17,7 @@ describe('PdsGoalCard', () => { , ); - expect(await findByText('$2,265.18')).toBeInTheDocument(); + expect(await findByText('$2,298.27')).toBeInTheDocument(); }); it('builds the View link with the PDS goal calculator path', async () => { diff --git a/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx b/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx index 7937838ce1..fd6208f74c 100644 --- a/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx +++ b/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx @@ -126,9 +126,15 @@ export const buildOtherBreakdownRows = ( { 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), + }, + ), amount: totals.assessment, testId: 'other-assessment', bold: true, diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.test.ts index bc6f01250f..33d738154d 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.test.ts @@ -145,11 +145,10 @@ describe('calculateOtherExpenses', () => { }); 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+470.64+444=8314.64; assessment = adminBase/0.88 - adminBase ≈ 1133.81 + expect(result.assessment).toBeCloseTo(1133.81, 1); }); it('returns 0 when adminRate is 0', () => { @@ -171,7 +170,7 @@ describe('calculateOtherExpenses', () => { 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.assessment).toBeCloseTo(1133.81, 1); }); it('produces correct totals for a part-time employee', () => { @@ -180,11 +179,12 @@ describe('calculateOtherExpenses', () => { // 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 + // adminBase=6580+418.49+394.80=7393.29 + // assessment = adminBase/0.88 - adminBase ≈ 1008.18 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.assessment).toBeCloseTo(1008.18, 1); }); }); }); diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.ts b/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.ts index c9240ae34b..89c817f6ab 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.ts @@ -55,8 +55,10 @@ export const calculateOtherExpenses = ( const attrition = subtotal * constants.attritionRate; const creditCardFees = (subtotal + attrition) * constants.creditCardFeeRate; - const adminRate = constants.adminRate; - const assessment = (subtotal + creditCardFees + attrition) * adminRate; + 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, diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts b/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts index 83632dea10..17977c8e2b 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts @@ -80,4 +80,3 @@ export const buildOtherExpensesConstants = ( adminRate: constants.adminRate, }; }; - diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts index 2f93b9a08c..b8384a9fac 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts @@ -384,5 +384,4 @@ describe('usePdsSummaryData', () => { ); }); }); - }); From 2057ac41a846ac8f72ab6b510d2d0ea65beb0d4e Mon Sep 17 00:00:00 2001 From: wjames111 Date: Wed, 13 May 2026 17:09:56 -0400 Subject: [PATCH 04/10] Fix tests --- .../GoalCard/PdsGoalCard.test.tsx | 2 +- .../PdsGoalCalculatorTestWrapper.tsx | 6 ++--- .../SupportItem/salaryBreakdown.test.tsx | 8 +++---- .../SupportItem/salaryBreakdown.tsx | 4 ++-- .../buildPdsGoalConstants.test.ts | 12 +++++----- .../calculations/pdsGoalConstants.ts | 4 +++- .../calculations/salaryCalculation.test.ts | 16 ++++++------- .../calculations/usePdsSummaryData.test.ts | 23 ++++++++++--------- 8 files changed, 39 insertions(+), 36 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx b/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx index 5385552eb0..9e6fd98306 100644 --- a/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx @@ -17,7 +17,7 @@ describe('PdsGoalCard', () => { , ); - expect(await findByText('$2,298.27')).toBeInTheDocument(); + expect(await findByText('$8,043.95')).toBeInTheDocument(); }); it('builds the View link with the PDS goal calculator path', async () => { diff --git a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx index ed9f228020..dfa4f67b90 100644 --- a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx +++ b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx @@ -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: [ diff --git a/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.test.tsx b/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.test.tsx index 41081d08eb..12fa8981e2 100644 --- a/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.test.tsx @@ -9,7 +9,7 @@ import { buildSalaryBreakdownRows, } from './salaryBreakdown'; -const constants = { geographicMultiplier: 0, employerFicaRate: 0.08 }; +const constants = { geographicMultiplier: 1, employerFicaRate: 0.08 }; const salariedCalculation: SalaryCalculationFields = { salaryOrHourly: DesignationSupportSalaryType.Salaried, @@ -70,8 +70,8 @@ describe('buildSalaryBreakdownRows', () => { // 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['geographic-multiplier']).toBe(1); + // grossMonthlyPay = 5000 * 1 = 5000 expect(byId['gross-monthly-pay']).toBe(5000); // employerFica = 5000 * 0.08 = 400 expect(byId['employer-fica']).toBe(400); @@ -94,7 +94,7 @@ describe('buildSalaryBreakdownRows', () => { expect(byId['hours-per-week']).toBe(40); // monthlyBase = (25 * 40 * 52) / 12 ≈ 4333.33 expect(byId['monthly-base']).toBeCloseTo(4333.33, 2); - // grossMonthlyPay = 4333.33 * (1 + 0) ≈ 4333.33 + // grossMonthlyPay = 4333.33 * 1 ≈ 4333.33 expect(byId['gross-monthly-pay']).toBeCloseTo(4333.33, 2); // employerFica = 4333.33 * 0.08 ≈ 346.67 expect(byId['employer-fica']).toBeCloseTo(346.67, 2); diff --git a/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.tsx b/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.tsx index cd8138c7bc..f76be25ee9 100644 --- a/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.tsx +++ b/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.tsx @@ -77,12 +77,12 @@ export const buildSalaryBreakdownRows = ( id: 'geographic-multiplier', category: t('Geographic Multiplier'), amount: geographicMultiplier, - format: 'percentage', + format: 'number', }, { id: 'gross-monthly-pay', category: t('Gross Monthly Pay'), - formula: t('Monthly Base × (1 + Geographic Multiplier)'), + formula: t('Monthly Base × Geographic Multiplier'), amount: grossMonthlyPay, format: 'currency', testId: 'gross-monthly-pay', diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/buildPdsGoalConstants.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/buildPdsGoalConstants.test.ts index a9b8b71729..2870431791 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/buildPdsGoalConstants.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/buildPdsGoalConstants.test.ts @@ -108,7 +108,7 @@ describe('buildPdsGoalConstants', () => { expect(result).toBeNull(); }); - it('returns geographicMultiplier of 0 for unknown location', () => { + it('defaults geographicMultiplier to 1 (no adjustment) for unknown location', () => { const result = buildPdsGoalConstants( buildMiscConstants(), defaultGeoMap, @@ -116,10 +116,10 @@ describe('buildPdsGoalConstants', () => { null, ); - expect(result?.geographicMultiplier).toBe(0); + expect(result?.geographicMultiplier).toBe(1); }); - it('returns geographicMultiplier of 0 when location is null', () => { + it('defaults geographicMultiplier to 1 (no adjustment) when location is null', () => { const result = buildPdsGoalConstants( buildMiscConstants(), defaultGeoMap, @@ -127,10 +127,10 @@ describe('buildPdsGoalConstants', () => { null, ); - expect(result?.geographicMultiplier).toBe(0); + expect(result?.geographicMultiplier).toBe(1); }); - it('returns geographicMultiplier of 0 when location is undefined', () => { + it('defaults geographicMultiplier to 1 (no adjustment) when location is undefined', () => { const result = buildPdsGoalConstants( buildMiscConstants(), defaultGeoMap, @@ -138,7 +138,7 @@ describe('buildPdsGoalConstants', () => { null, ); - expect(result?.geographicMultiplier).toBe(0); + expect(result?.geographicMultiplier).toBe(1); }); it('looks up correct geographic multiplier', () => { diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts b/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts index 17977c8e2b..d1a1ba702e 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts @@ -44,8 +44,10 @@ export const buildPdsGoalConstants = ( return null; } + // Multiplier is the *full* factor applied to monthlyBase (e.g. 1.06 for a + // 6% high-cost location), so the no-adjustment default must be 1, not 0. const geographicMultiplier = - goalGeographicConstantMap.get(geographicLocation ?? '') ?? 0; + goalGeographicConstantMap.get(geographicLocation ?? '') ?? 1; const taxDeferredPct = (fourOThreeB?.currentTaxDeferredContributionPercentage ?? 0) / 100; diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.test.ts index c78088deed..d8904fca35 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.test.ts @@ -5,7 +5,7 @@ import { } from './salaryCalculation'; const FICA_RATE = 0.08; -const GEO_MULTIPLIER = 0.06; +const GEO_MULTIPLIER = 1.06; const salaried = ( overrides: Partial = {}, @@ -32,14 +32,14 @@ describe('calculateSalaryTotals', () => { describe('salaried', () => { it('divides yearly payRate by 12 when there is no geographic multiplier', () => { const result = calculateSalaryTotals(salaried(), { - geographicMultiplier: 0, + geographicMultiplier: 1, employerFicaRate: FICA_RATE, }); // 60000 / 12 expect(result.grossMonthlyPay).toBe(5000); }); - it('applies geographic multiplier additively', () => { + it('applies geographic multiplier as a full factor', () => { const result = calculateSalaryTotals(salaried(), { geographicMultiplier: GEO_MULTIPLIER, employerFicaRate: FICA_RATE, @@ -51,7 +51,7 @@ describe('calculateSalaryTotals', () => { it('ignores hoursWorkedPerWeek', () => { const result = calculateSalaryTotals( salaried({ hoursWorkedPerWeek: 40 }), - { geographicMultiplier: 0, employerFicaRate: FICA_RATE }, + { geographicMultiplier: 1, employerFicaRate: FICA_RATE }, ); expect(result.grossMonthlyPay).toBe(5000); }); @@ -60,7 +60,7 @@ describe('calculateSalaryTotals', () => { describe('hourly', () => { it('converts hourly rate to monthly when there is no geographic multiplier', () => { const result = calculateSalaryTotals(hourly(), { - geographicMultiplier: 0, + geographicMultiplier: 1, employerFicaRate: FICA_RATE, }); // 25 * 40 * 52 / 12 @@ -91,7 +91,7 @@ describe('calculateSalaryTotals', () => { it('treats null hoursWorkedPerWeek as 0 when hourly', () => { const result = calculateSalaryTotals( hourly({ hoursWorkedPerWeek: null }), - { geographicMultiplier: 0, employerFicaRate: FICA_RATE }, + { geographicMultiplier: 1, employerFicaRate: FICA_RATE }, ); expect(result.grossMonthlyPay).toBe(0); }); @@ -100,7 +100,7 @@ describe('calculateSalaryTotals', () => { describe('employer FICA and subtotal', () => { it('multiplies grossMonthlyPay by the provided FICA rate', () => { const result = calculateSalaryTotals(salaried(), { - geographicMultiplier: 0, + geographicMultiplier: 1, employerFicaRate: FICA_RATE, }); expect(result.employerFica).toBeCloseTo(400); @@ -108,7 +108,7 @@ describe('calculateSalaryTotals', () => { it('uses the passed-in FICA rate verbatim (no fallback applied)', () => { const result = calculateSalaryTotals(salaried(), { - geographicMultiplier: 0, + geographicMultiplier: 1, employerFicaRate: 0.0765, }); expect(result.employerFica).toBeCloseTo(382.5); diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts index b8384a9fac..af7a2b960b 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts @@ -35,7 +35,7 @@ const WORK_COMP_PERCENTAGE = 0.17; const ATTRITION_RATE = 0.06; const CREDIT_CARD_FEE_RATE = 0.06; const ADMIN_RATE = 0.12; -const GEO_MULTIPLIER = 0.06; +const GEO_MULTIPLIER = 1.06; const constantsMock = gqlMock( GoalCalculatorConstantsDocument, @@ -71,7 +71,7 @@ const constantsMock = gqlMock( ], mpdGoalGeographicConstants: [ { location: 'Orlando, FL', percentageMultiplier: GEO_MULTIPLIER }, - { location: 'None', percentageMultiplier: 0 }, + { location: 'None', percentageMultiplier: 1 }, ], mpdGoalBenefitsConstants: [], }, @@ -212,14 +212,14 @@ describe('usePdsSummaryData', () => { expect(result.current?.geographicMultiplier).toBe(GEO_MULTIPLIER); }); - it('defaults to 0 when geographicLocation is null', () => { + it('defaults to 1 (no adjustment) when geographicLocation is null', () => { const { result } = renderHook(() => usePdsSummaryData(defaultCalculation, defaultHcmUser), ); - expect(result.current?.geographicMultiplier).toBe(0); + expect(result.current?.geographicMultiplier).toBe(1); }); - it('defaults to 0 when geographicLocation is not in the map', () => { + it('defaults to 1 (no adjustment) when geographicLocation is not in the map', () => { const calc = { ...defaultCalculation, geographicLocation: 'Unknown City', @@ -227,7 +227,7 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(calc, defaultHcmUser), ); - expect(result.current?.geographicMultiplier).toBe(0); + expect(result.current?.geographicMultiplier).toBe(1); }); }); @@ -296,8 +296,8 @@ describe('usePdsSummaryData', () => { }); it('computes correct overallTotal for a full-time salaried employee', () => { - // No geographic multiplier, payRate = 60000 - // grossMonthlyPay = 60000 / 12 = 5000 + // Geographic multiplier defaults to 1 (no adjustment), payRate = 60000 + // grossMonthlyPay = 60000 / 12 * 1 = 5000 // employerFica = 5000 * 0.08 = 400 // salarySubtotal = 5400 // @@ -310,12 +310,13 @@ describe('usePdsSummaryData', () => { // otherSubtotal = 5400 + 500 + 400 + 0 + 1500 = 7800 // attrition = 7800 * 0.06 = 468 // creditCardFees = (7800 + 468) * 0.06 = 496.08 - // assessment = (7800 + 468 + 496.08) * 0.12 = 1051.69 - // overallTotal = 7800 + 468 + 496.08 + 1051.69 = 9815.77 + // adminBase = 7800 + 468 + 496.08 = 8764.08 + // assessment = adminBase / 0.88 - adminBase ≈ 1195.10 + // overallTotal = 7800 + 468 + 496.08 + 1195.10 ≈ 9959.18 const { result } = renderHook(() => usePdsSummaryData(defaultCalculation, defaultHcmUser), ); - expect(result.current?.overallTotal).toBeCloseTo(9815.77, 0); + expect(result.current?.overallTotal).toBeCloseTo(9959.18, 0); }); }); From 6ab40c7023272b2b1345e17a318cae334fc31030 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Thu, 14 May 2026 14:22:16 -0400 Subject: [PATCH 05/10] Fix calculations --- .../HrTools/PdsGoalCalculator/calculations/OtherExpenses.ts | 4 +++- .../PdsGoalCalculator/calculations/pdsGoalConstants.ts | 1 - .../PdsGoalCalculator/calculations/salaryCalculation.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.ts b/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.ts index 89c817f6ab..15068d8a77 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.ts @@ -54,7 +54,9 @@ export const calculateOtherExpenses = ( benefits; const attrition = subtotal * constants.attritionRate; - const creditCardFees = (subtotal + attrition) * constants.creditCardFeeRate; + 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. diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts b/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts index d1a1ba702e..ef8379a914 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts @@ -43,7 +43,6 @@ export const buildPdsGoalConstants = ( ) { return null; } - // Multiplier is the *full* factor applied to monthlyBase (e.g. 1.06 for a // 6% high-cost location), so the no-adjustment default must be 1, not 0. const geographicMultiplier = diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.ts b/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.ts index 9be7972240..d47af4622d 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.ts @@ -31,7 +31,7 @@ export const calculateSalaryTotals = ( calculation.salaryOrHourly === DesignationSupportSalaryType.Salaried; const monthlyBase = isSalaried ? payRate / 12 : (payRate * hours * 52) / 12; - const grossMonthlyPay = monthlyBase * geographicMultiplier; + const grossMonthlyPay = monthlyBase * (1 + geographicMultiplier); const employerFica = grossMonthlyPay * employerFicaRate; const subtotal = grossMonthlyPay + employerFica; From 4136ea8029ba1bde4f595fc102abcb74f28075a6 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Thu, 14 May 2026 16:23:49 -0400 Subject: [PATCH 06/10] fix tests --- .../GoalCard/PdsGoalCard.test.tsx | 2 +- .../SupportItem/salaryBreakdown.test.tsx | 10 +++---- .../SupportItem/salaryBreakdown.tsx | 10 +++---- .../calculations/OtherExpenses.test.ts | 24 ++++++++--------- .../buildPdsGoalConstants.test.ts | 12 ++++----- .../calculations/pdsGoalConstants.ts | 4 +-- .../calculations/salaryCalculation.test.ts | 20 +++++++------- .../calculations/usePdsSummaryData.test.ts | 26 +++++++++---------- 8 files changed, 49 insertions(+), 59 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx b/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx index 9e6fd98306..8355678e28 100644 --- a/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx @@ -17,7 +17,7 @@ describe('PdsGoalCard', () => { , ); - expect(await findByText('$8,043.95')).toBeInTheDocument(); + expect(await findByText('$8,073.02')).toBeInTheDocument(); }); it('builds the View link with the PDS goal calculator path', async () => { diff --git a/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.test.tsx b/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.test.tsx index 12fa8981e2..c12a0a874e 100644 --- a/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.test.tsx @@ -9,7 +9,7 @@ import { buildSalaryBreakdownRows, } from './salaryBreakdown'; -const constants = { geographicMultiplier: 1, employerFicaRate: 0.08 }; +const constants = { geographicMultiplier: 0, employerFicaRate: 0.08 }; const salariedCalculation: SalaryCalculationFields = { salaryOrHourly: DesignationSupportSalaryType.Salaried, @@ -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', @@ -69,9 +68,7 @@ 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(1); - // grossMonthlyPay = 5000 * 1 = 5000 + // grossMonthlyPay = 5000 * (1 + 0) = 5000 expect(byId['gross-monthly-pay']).toBe(5000); // employerFica = 5000 * 0.08 = 400 expect(byId['employer-fica']).toBe(400); @@ -94,7 +91,7 @@ describe('buildSalaryBreakdownRows', () => { expect(byId['hours-per-week']).toBe(40); // monthlyBase = (25 * 40 * 52) / 12 ≈ 4333.33 expect(byId['monthly-base']).toBeCloseTo(4333.33, 2); - // grossMonthlyPay = 4333.33 * 1 ≈ 4333.33 + // grossMonthlyPay = 4333.33 * (1 + 0) ≈ 4333.33 expect(byId['gross-monthly-pay']).toBeCloseTo(4333.33, 2); // employerFica = 4333.33 * 0.08 ≈ 346.67 expect(byId['employer-fica']).toBeCloseTo(346.67, 2); @@ -113,7 +110,6 @@ describe('buildSalaryBreakdownRows', () => { 'pay-rate', 'hours-per-week', 'monthly-base', - 'geographic-multiplier', 'gross-monthly-pay', 'employer-fica', 'total', diff --git a/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.tsx b/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.tsx index f76be25ee9..f8b4e60bda 100644 --- a/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.tsx +++ b/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.tsx @@ -73,16 +73,12 @@ export const buildSalaryBreakdownRows = ( amount: monthlyBase, format: 'currency', }, - { - id: 'geographic-multiplier', - category: t('Geographic Multiplier'), - amount: geographicMultiplier, - format: 'number', - }, { id: 'gross-monthly-pay', category: t('Gross Monthly Pay'), - formula: t('Monthly Base × Geographic Multiplier'), + formula: t('Monthly Base × {{rate}}', { + rate: percentageFormat(geographicMultiplier, locale), + }), amount: grossMonthlyPay, format: 'currency', testId: 'gross-monthly-pay', diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.test.ts index 33d738154d..485fdf43b5 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.test.ts @@ -137,18 +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('grosses up (subtotal + creditCardFees + attrition) so that admin is `adminRate` of the post-admin total', () => { const result = calculateOtherExpenses(fullTime(), defaultConstants); - // adminBase=7400+470.64+444=8314.64; assessment = adminBase/0.88 - adminBase ≈ 1133.81 - expect(result.assessment).toBeCloseTo(1133.81, 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', () => { @@ -169,8 +169,8 @@ 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(1133.81, 1); + expect(result.creditCardFees).toBeCloseTo(500.68); + expect(result.assessment).toBeCloseTo(1137.91, 1); }); it('produces correct totals for a part-time employee', () => { @@ -178,13 +178,13 @@ describe('calculateOtherExpenses', () => { // 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 - // adminBase=6580+418.49+394.80=7393.29 - // assessment = adminBase/0.88 - adminBase ≈ 1008.18 + // 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(1008.18, 1); + expect(result.creditCardFees).toBeCloseTo(445.2, 1); + expect(result.assessment).toBeCloseTo(1011.82, 1); }); }); }); diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/buildPdsGoalConstants.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/buildPdsGoalConstants.test.ts index 2870431791..939def989b 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/buildPdsGoalConstants.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/buildPdsGoalConstants.test.ts @@ -108,7 +108,7 @@ describe('buildPdsGoalConstants', () => { expect(result).toBeNull(); }); - it('defaults geographicMultiplier to 1 (no adjustment) for unknown location', () => { + it('defaults geographicMultiplier to 0 (no adjustment) for unknown location', () => { const result = buildPdsGoalConstants( buildMiscConstants(), defaultGeoMap, @@ -116,10 +116,10 @@ describe('buildPdsGoalConstants', () => { null, ); - expect(result?.geographicMultiplier).toBe(1); + expect(result?.geographicMultiplier).toBe(0); }); - it('defaults geographicMultiplier to 1 (no adjustment) when location is null', () => { + it('defaults geographicMultiplier to 0 (no adjustment) when location is null', () => { const result = buildPdsGoalConstants( buildMiscConstants(), defaultGeoMap, @@ -127,10 +127,10 @@ describe('buildPdsGoalConstants', () => { null, ); - expect(result?.geographicMultiplier).toBe(1); + expect(result?.geographicMultiplier).toBe(0); }); - it('defaults geographicMultiplier to 1 (no adjustment) when location is undefined', () => { + it('defaults geographicMultiplier to 0 (no adjustment) when location is undefined', () => { const result = buildPdsGoalConstants( buildMiscConstants(), defaultGeoMap, @@ -138,7 +138,7 @@ describe('buildPdsGoalConstants', () => { null, ); - expect(result?.geographicMultiplier).toBe(1); + expect(result?.geographicMultiplier).toBe(0); }); it('looks up correct geographic multiplier', () => { diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts b/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts index ef8379a914..360d557f91 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/pdsGoalConstants.ts @@ -43,10 +43,8 @@ export const buildPdsGoalConstants = ( ) { return null; } - // Multiplier is the *full* factor applied to monthlyBase (e.g. 1.06 for a - // 6% high-cost location), so the no-adjustment default must be 1, not 0. const geographicMultiplier = - goalGeographicConstantMap.get(geographicLocation ?? '') ?? 1; + goalGeographicConstantMap.get(geographicLocation ?? '') ?? 0; const taxDeferredPct = (fourOThreeB?.currentTaxDeferredContributionPercentage ?? 0) / 100; diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.test.ts index d8904fca35..4f3d2cabd6 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/salaryCalculation.test.ts @@ -5,7 +5,7 @@ import { } from './salaryCalculation'; const FICA_RATE = 0.08; -const GEO_MULTIPLIER = 1.06; +const GEO_MULTIPLIER = 0.06; const salaried = ( overrides: Partial = {}, @@ -32,26 +32,26 @@ describe('calculateSalaryTotals', () => { describe('salaried', () => { it('divides yearly payRate by 12 when there is no geographic multiplier', () => { const result = calculateSalaryTotals(salaried(), { - geographicMultiplier: 1, + geographicMultiplier: 0, employerFicaRate: FICA_RATE, }); // 60000 / 12 expect(result.grossMonthlyPay).toBe(5000); }); - it('applies geographic multiplier as a full factor', () => { + it('applies geographic multiplier as a delta to the monthly base', () => { const result = calculateSalaryTotals(salaried(), { geographicMultiplier: GEO_MULTIPLIER, employerFicaRate: FICA_RATE, }); - // (60000 / 12) * 1.06 + // (60000 / 12) * (1 + 0.06) expect(result.grossMonthlyPay).toBeCloseTo(5300); }); it('ignores hoursWorkedPerWeek', () => { const result = calculateSalaryTotals( salaried({ hoursWorkedPerWeek: 40 }), - { geographicMultiplier: 1, employerFicaRate: FICA_RATE }, + { geographicMultiplier: 0, employerFicaRate: FICA_RATE }, ); expect(result.grossMonthlyPay).toBe(5000); }); @@ -60,7 +60,7 @@ describe('calculateSalaryTotals', () => { describe('hourly', () => { it('converts hourly rate to monthly when there is no geographic multiplier', () => { const result = calculateSalaryTotals(hourly(), { - geographicMultiplier: 1, + geographicMultiplier: 0, employerFicaRate: FICA_RATE, }); // 25 * 40 * 52 / 12 @@ -72,7 +72,7 @@ describe('calculateSalaryTotals', () => { geographicMultiplier: GEO_MULTIPLIER, employerFicaRate: FICA_RATE, }); - // (25 * 40 * 52 / 12) * 1.06 + // (25 * 40 * 52 / 12) * (1 + 0.06) expect(result.grossMonthlyPay).toBeCloseTo(4593.333, 2); }); }); @@ -91,7 +91,7 @@ describe('calculateSalaryTotals', () => { it('treats null hoursWorkedPerWeek as 0 when hourly', () => { const result = calculateSalaryTotals( hourly({ hoursWorkedPerWeek: null }), - { geographicMultiplier: 1, employerFicaRate: FICA_RATE }, + { geographicMultiplier: 0, employerFicaRate: FICA_RATE }, ); expect(result.grossMonthlyPay).toBe(0); }); @@ -100,7 +100,7 @@ describe('calculateSalaryTotals', () => { describe('employer FICA and subtotal', () => { it('multiplies grossMonthlyPay by the provided FICA rate', () => { const result = calculateSalaryTotals(salaried(), { - geographicMultiplier: 1, + geographicMultiplier: 0, employerFicaRate: FICA_RATE, }); expect(result.employerFica).toBeCloseTo(400); @@ -108,7 +108,7 @@ describe('calculateSalaryTotals', () => { it('uses the passed-in FICA rate verbatim (no fallback applied)', () => { const result = calculateSalaryTotals(salaried(), { - geographicMultiplier: 1, + geographicMultiplier: 0, employerFicaRate: 0.0765, }); expect(result.employerFica).toBeCloseTo(382.5); diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts index af7a2b960b..d5bfc1e250 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts @@ -35,7 +35,7 @@ const WORK_COMP_PERCENTAGE = 0.17; const ATTRITION_RATE = 0.06; const CREDIT_CARD_FEE_RATE = 0.06; const ADMIN_RATE = 0.12; -const GEO_MULTIPLIER = 1.06; +const GEO_MULTIPLIER = 0.06; const constantsMock = gqlMock( GoalCalculatorConstantsDocument, @@ -71,7 +71,7 @@ const constantsMock = gqlMock( ], mpdGoalGeographicConstants: [ { location: 'Orlando, FL', percentageMultiplier: GEO_MULTIPLIER }, - { location: 'None', percentageMultiplier: 1 }, + { location: 'None', percentageMultiplier: 0 }, ], mpdGoalBenefitsConstants: [], }, @@ -212,14 +212,14 @@ describe('usePdsSummaryData', () => { expect(result.current?.geographicMultiplier).toBe(GEO_MULTIPLIER); }); - it('defaults to 1 (no adjustment) when geographicLocation is null', () => { + it('defaults to 0 (no adjustment) when geographicLocation is null', () => { const { result } = renderHook(() => usePdsSummaryData(defaultCalculation, defaultHcmUser), ); - expect(result.current?.geographicMultiplier).toBe(1); + expect(result.current?.geographicMultiplier).toBe(0); }); - it('defaults to 1 (no adjustment) when geographicLocation is not in the map', () => { + it('defaults to 0 (no adjustment) when geographicLocation is not in the map', () => { const calc = { ...defaultCalculation, geographicLocation: 'Unknown City', @@ -227,7 +227,7 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(calc, defaultHcmUser), ); - expect(result.current?.geographicMultiplier).toBe(1); + expect(result.current?.geographicMultiplier).toBe(0); }); }); @@ -296,8 +296,8 @@ describe('usePdsSummaryData', () => { }); it('computes correct overallTotal for a full-time salaried employee', () => { - // Geographic multiplier defaults to 1 (no adjustment), payRate = 60000 - // grossMonthlyPay = 60000 / 12 * 1 = 5000 + // Geographic multiplier defaults to 0 (no adjustment), payRate = 60000 + // grossMonthlyPay = 60000 / 12 * (1 + 0) = 5000 // employerFica = 5000 * 0.08 = 400 // salarySubtotal = 5400 // @@ -309,14 +309,14 @@ describe('usePdsSummaryData', () => { // workComp = 0 (full-time) // otherSubtotal = 5400 + 500 + 400 + 0 + 1500 = 7800 // attrition = 7800 * 0.06 = 468 - // creditCardFees = (7800 + 468) * 0.06 = 496.08 - // adminBase = 7800 + 468 + 496.08 = 8764.08 - // assessment = adminBase / 0.88 - adminBase ≈ 1195.10 - // overallTotal = 7800 + 468 + 496.08 + 1195.10 ≈ 9959.18 + // creditCardFees = (7800 + 468) / (1 - 0.06) - (7800 + 468) ≈ 527.74 + // adminBase = 7800 + 468 + 527.74 ≈ 8795.74 + // assessment = adminBase / 0.88 - adminBase ≈ 1199.42 + // overallTotal = 7800 + 468 + 527.74 + 1199.42 ≈ 9995.16 const { result } = renderHook(() => usePdsSummaryData(defaultCalculation, defaultHcmUser), ); - expect(result.current?.overallTotal).toBeCloseTo(9959.18, 0); + expect(result.current?.overallTotal).toBeCloseTo(9995.16, 0); }); }); From ead7b68246e11f2ddbb404b24e729dff213ae025 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Thu, 14 May 2026 16:33:52 -0400 Subject: [PATCH 07/10] Fix The Gross Monthly Pay display formula --- .../HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.tsx b/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.tsx index f8b4e60bda..80c3330382 100644 --- a/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.tsx +++ b/src/components/HrTools/PdsGoalCalculator/SupportItem/salaryBreakdown.tsx @@ -77,7 +77,7 @@ export const buildSalaryBreakdownRows = ( id: 'gross-monthly-pay', category: t('Gross Monthly Pay'), formula: t('Monthly Base × {{rate}}', { - rate: percentageFormat(geographicMultiplier, locale), + rate: percentageFormat(1 + geographicMultiplier, locale), }), amount: grossMonthlyPay, format: 'currency', From c0950020e8f8114df33d6060570d9a327e0195c1 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Fri, 15 May 2026 11:20:04 -0400 Subject: [PATCH 08/10] PR fixes --- .../PdsGoalCalculatorTestWrapper.tsx | 6 +++--- .../SupportItem/otherBreakdown.tsx | 16 ++++++++-------- .../calculations/usePdsSummaryData.test.ts | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx index dfa4f67b90..ed9f228020 100644 --- a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx +++ b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx @@ -194,15 +194,15 @@ export const PdsGoalCalculatorTestWrapper: React.FC< mpdGoalGeographicConstants: [ { location: 'None', - percentageMultiplier: 1, + percentageMultiplier: 0, }, { location: 'Orlando, FL', - percentageMultiplier: 1.06, + percentageMultiplier: 0.06, }, { location: 'New York, NY', - percentageMultiplier: 1.12, + percentageMultiplier: 0.12, }, ], mpdGoalMiscConstants: [ diff --git a/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx b/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx index fd6208f74c..b2a8bbdaed 100644 --- a/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx +++ b/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx @@ -116,9 +116,12 @@ export const buildOtherBreakdownRows = ( { 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)', + { + rate: percentageFormat(constants.creditCardFeeRate, locale), + }, + ), amount: totals.creditCardFees, testId: 'other-credit-card-fees', bold: true, @@ -127,12 +130,9 @@ export const buildOtherBreakdownRows = ( id: 'assessment', category: t('Assessment'), formula: t( - '(Subtotal + Attrition + Credit Card Fees) ÷ {{divisor}} − (Subtotal + Attrition + Credit Card Fees)', + '(Subtotal + Attrition + Credit Card Fees) ÷ (1 − {{rate}}) − (Subtotal + Attrition + Credit Card Fees)', { - divisor: new Intl.NumberFormat(locale, { - minimumFractionDigits: 2, - maximumFractionDigits: 4, - }).format(1 - constants.adminRate), + rate: percentageFormat(constants.adminRate, locale), }, ), amount: totals.assessment, diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts index d5bfc1e250..cc65aadc75 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts @@ -318,6 +318,23 @@ describe('usePdsSummaryData', () => { ); expect(result.current?.overallTotal).toBeCloseTo(9995.16, 0); }); + + it('computes correct overallTotal for a part-time hourly employee', () => { + const calc = { + ...defaultCalculation, + salaryOrHourly: DesignationSupportSalaryType.Hourly, + status: DesignationSupportStatus.PartTime, + payRate: 25, + hoursWorkedPerWeek: 20, + benefits: null, + }; + const { result } = renderHook(() => + usePdsSummaryData(calc, defaultHcmUser), + ); + expect(result.current?.overallTotal).toBeGreaterThan(0); + expect(result.current?.otherTotals.workComp).toBeGreaterThan(0); + expect(result.current?.otherTotals.benefits).toBe(0); + }); }); describe('result shape', () => { From b2c201cfcdb713afe1d999021f94ed7179b18963 Mon Sep 17 00:00:00 2001 From: wjames111 Date: Fri, 15 May 2026 12:32:54 -0400 Subject: [PATCH 09/10] More PR fixes --- .../GoalCard/PdsGoalCard.test.tsx | 14 +++ .../GoalCard/PdsGoalCard.tsx | 9 +- .../PdsGoalCalculatorTestWrapper.tsx | 22 ++++ .../Shared/PdsGoalCalculatorContext.tsx | 2 +- .../calculations/OtherExpenses.test.ts | 12 +- .../calculations/OtherExpenses.ts | 2 +- .../calculations/usePdsSummaryData.test.ts | 115 +++++++++++++----- .../calculations/usePdsSummaryData.ts | 15 ++- 8 files changed, 148 insertions(+), 43 deletions(-) diff --git a/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx b/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx index 8355678e28..61d189229e 100644 --- a/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx +++ b/src/components/HrTools/PdsGoalCalculator/GoalCard/PdsGoalCard.test.tsx @@ -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( = ({ goal }) => { const accountListId = useAccountListId() ?? ''; const [deletePdsGoalCalculation] = useDeletePdsGoalCalculationMutation(); - const { loading: constantsLoading } = useGoalCalculatorConstants(); const { data: hcmData, loading: hcmLoading } = useHcmUserQuery(); const hcmUser = hcmData?.hcm[0]; - const summaryData = usePdsSummaryData(goal, hcmUser); + const { data: summaryData, loading: summaryLoading } = usePdsSummaryData( + goal, + hcmUser, + ); const goalTotal = summaryData?.overallTotal ?? 0; const formType = goal.formType ?? DesignationSupportFormType.Detailed; @@ -51,7 +52,7 @@ export const PdsGoalCard: React.FC = ({ 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} diff --git a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx index ed9f228020..728bdb74f4 100644 --- a/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx +++ b/src/components/HrTools/PdsGoalCalculator/PdsGoalCalculatorTestWrapper.tsx @@ -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: { @@ -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' }, diff --git a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx index c96bb9025d..f2442c6947 100644 --- a/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx +++ b/src/components/HrTools/PdsGoalCalculator/Shared/PdsGoalCalculatorContext.tsx @@ -89,7 +89,7 @@ export const PdsGoalCalculatorProvider: React.FC = ({ 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 diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.test.ts index 485fdf43b5..8b91b820ef 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.test.ts @@ -142,12 +142,20 @@ describe('calculateOtherExpenses', () => { // (7400 + 444) / (1 - 0.06) - (7400 + 444) ≈ 500.68 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('grosses up (subtotal + creditCardFees + attrition) so that admin is `adminRate` of the post-admin total', () => { + it('grosses up (subtotal + attrition + creditCardFees) so that admin is `adminRate` of the post-admin total', () => { const result = calculateOtherExpenses(fullTime(), defaultConstants); - // adminBase=7400+500.68+444=8344.68; assessment = adminBase/0.88 - adminBase ≈ 1137.91 + // adminBase=7400+444+500.68=8344.68; assessment = adminBase/0.88 - adminBase ≈ 1137.91 expect(result.assessment).toBeCloseTo(1137.91, 1); }); diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.ts b/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.ts index 15068d8a77..2c87b99773 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/OtherExpenses.ts @@ -57,7 +57,7 @@ export const calculateOtherExpenses = ( const creditCardFees = (subtotal + attrition) / (1 - constants.creditCardFeeRate) - (subtotal + attrition); - const adminBase = subtotal + creditCardFees + 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; diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts index cc65aadc75..9cb8a06714 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.test.ts @@ -137,7 +137,7 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(undefined, defaultHcmUser), ); - expect(result.current).toBeNull(); + expect(result.current.data).toBeNull(); }); it.each([ @@ -196,7 +196,7 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(defaultCalculation, defaultHcmUser), ); - expect(result.current).toBeNull(); + expect(result.current.data).toBeNull(); }); }); @@ -209,14 +209,14 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(calc, defaultHcmUser), ); - expect(result.current?.geographicMultiplier).toBe(GEO_MULTIPLIER); + expect(result.current.data?.geographicMultiplier).toBe(GEO_MULTIPLIER); }); it('defaults to 0 (no adjustment) when geographicLocation is null', () => { const { result } = renderHook(() => usePdsSummaryData(defaultCalculation, defaultHcmUser), ); - expect(result.current?.geographicMultiplier).toBe(0); + expect(result.current.data?.geographicMultiplier).toBe(0); }); it('defaults to 0 (no adjustment) when geographicLocation is not in the map', () => { @@ -227,7 +227,7 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(calc, defaultHcmUser), ); - expect(result.current?.geographicMultiplier).toBe(0); + expect(result.current.data?.geographicMultiplier).toBe(0); }); }); @@ -240,7 +240,7 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(calc, defaultHcmUser), ); - expect(result.current?.salaryConstants).toEqual({ + expect(result.current.data?.salaryConstants).toEqual({ geographicMultiplier: GEO_MULTIPLIER, employerFicaRate: EMPLOYER_FICA_RATE, }); @@ -253,16 +253,16 @@ describe('usePdsSummaryData', () => { usePdsSummaryData(defaultCalculation, defaultHcmUser), ); // (5 + 3) / 100 = 0.08 - expect(result.current?.otherConstants.fourOThreeBPercentage).toBeCloseTo( - 0.08, - ); + expect( + result.current.data?.otherConstants.fourOThreeBPercentage, + ).toBeCloseTo(0.08); }); it('defaults to 0 when hcmUser is undefined', () => { const { result } = renderHook(() => usePdsSummaryData(defaultCalculation, undefined), ); - expect(result.current?.otherConstants.fourOThreeBPercentage).toBe(0); + expect(result.current.data?.otherConstants.fourOThreeBPercentage).toBe(0); }); it('defaults to 0 when contribution percentages are null', () => { @@ -277,7 +277,7 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(defaultCalculation, hcmUser), ); - expect(result.current?.otherConstants.fourOThreeBPercentage).toBe(0); + expect(result.current.data?.otherConstants.fourOThreeBPercentage).toBe(0); }); }); @@ -286,7 +286,7 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(defaultCalculation, defaultHcmUser), ); - const data = result.current!; + const data = result.current.data!; const expected = data.otherTotals.subtotal + data.otherTotals.attrition + @@ -316,10 +316,27 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(defaultCalculation, defaultHcmUser), ); - expect(result.current?.overallTotal).toBeCloseTo(9995.16, 0); + expect(result.current.data?.overallTotal).toBeCloseTo(9995.16, 0); }); it('computes correct overallTotal for a part-time hourly employee', () => { + // No geographic multiplier (defaults to 0), payRate = 25, hours = 20 + // monthlyBase = (25 * 20 * 52) / 12 = 2166.667 + // grossMonthlyPay = 2166.667 (no geo) + // employerFica = 2166.667 * 0.08 = 173.333 + // salarySubtotal = 2340 + // + // Reimbursable: monthly = 400, annual = 1200, raw = 500, above floor + // + // 403b = 2166.667 * 0.08 = 173.333 + // workComp = 2166.667 * 0.17 = 368.333 (part-time) + // benefits = 0 (part-time, ignores calculation.benefits) + // subtotal = 2340 + 500 + 173.333 + 368.333 + 0 = 3381.667 + // attrition = 3381.667 * 0.06 = 202.9 + // creditCardFees = (3381.667 + 202.9) / (1 - 0.06) - (3381.667 + 202.9) ≈ 228.80 + // adminBase ≈ 3813.37 + // assessment = adminBase / (1 - 0.12) - adminBase ≈ 520.00 + // overallTotal ≈ 3381.667 + 202.9 + 228.80 + 520.00 ≈ 4333.37 const calc = { ...defaultCalculation, salaryOrHourly: DesignationSupportSalaryType.Hourly, @@ -331,9 +348,43 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(calc, defaultHcmUser), ); - expect(result.current?.overallTotal).toBeGreaterThan(0); - expect(result.current?.otherTotals.workComp).toBeGreaterThan(0); - expect(result.current?.otherTotals.benefits).toBe(0); + expect(result.current.data?.overallTotal).toBeCloseTo(4333.37, 0); + expect(result.current.data?.otherTotals.workComp).toBeCloseTo(368.33, 2); + expect(result.current.data?.otherTotals.benefits).toBe(0); + }); + + it('does not NaN-cascade when payRate is null', () => { + // Guards against a regression where a null payRate would propagate + // through the gross-up formulas (x / (1 - rate)) and produce NaN. + const calc = { + ...defaultCalculation, + payRate: null, + }; + const { result } = renderHook(() => + usePdsSummaryData(calc, defaultHcmUser), + ); + const data = result.current.data!; + expect(data.salaryTotals.grossMonthlyPay).toBe(0); + expect(Number.isFinite(data.otherTotals.creditCardFees)).toBe(true); + expect(Number.isFinite(data.otherTotals.assessment)).toBe(true); + expect(Number.isFinite(data.overallTotal)).toBe(true); + }); + + it('applies the geographic multiplier to the overall total', () => { + const { result: baseline } = renderHook(() => + usePdsSummaryData(defaultCalculation, defaultHcmUser), + ); + const { result: withGeo } = renderHook(() => + usePdsSummaryData( + { ...defaultCalculation, geographicLocation: 'Orlando, FL' }, + defaultHcmUser, + ), + ); + expect(baseline.current.data?.geographicMultiplier).toBe(0); + expect(withGeo.current.data?.geographicMultiplier).toBe(GEO_MULTIPLIER); + expect(withGeo.current.data!.overallTotal).toBeGreaterThan( + baseline.current.data!.overallTotal, + ); }); }); @@ -342,7 +393,7 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(defaultCalculation, defaultHcmUser), ); - const data = result.current!; + const data = result.current.data!; expect(data).toHaveProperty('salaryTotals'); expect(data).toHaveProperty('salaryConstants'); expect(data).toHaveProperty('reimbursableTotals'); @@ -362,12 +413,12 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(calc, defaultHcmUser), ); - expect(result.current?.otherConstants.reimbursableTotal).toBe(0); - expect(result.current?.otherConstants.fourOThreeBPercentage).toBe(0); + expect(result.current.data?.otherConstants.reimbursableTotal).toBe(0); + expect(result.current.data?.otherConstants.fourOThreeBPercentage).toBe(0); // The reimbursableTotals object is still computed (data isn't lost), // but the otherConstants.reimbursableTotal that feeds the Other Expenses // calculation is zeroed. - expect(result.current?.reimbursableTotals.total).toBeGreaterThan(0); + expect(result.current.data?.reimbursableTotals.total).toBeGreaterThan(0); }); it('uses saved reimbursable + 403b values when formType is Detailed', () => { @@ -378,12 +429,12 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(calc, defaultHcmUser), ); - expect(result.current?.otherConstants.reimbursableTotal).toBeGreaterThan( - 0, - ); - expect(result.current?.otherConstants.fourOThreeBPercentage).toBeCloseTo( - 0.08, - ); + expect( + result.current.data?.otherConstants.reimbursableTotal, + ).toBeGreaterThan(0); + expect( + result.current.data?.otherConstants.fourOThreeBPercentage, + ).toBeCloseTo(0.08); }); it('uses saved values when formType is null/undefined (legacy goals default to Detailed behavior)', () => { @@ -394,12 +445,12 @@ describe('usePdsSummaryData', () => { const { result } = renderHook(() => usePdsSummaryData(calc, defaultHcmUser), ); - expect(result.current?.otherConstants.reimbursableTotal).toBeGreaterThan( - 0, - ); - expect(result.current?.otherConstants.fourOThreeBPercentage).toBeCloseTo( - 0.08, - ); + expect( + result.current.data?.otherConstants.reimbursableTotal, + ).toBeGreaterThan(0); + expect( + result.current.data?.otherConstants.fourOThreeBPercentage, + ).toBeCloseTo(0.08); }); }); }); diff --git a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.ts b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.ts index 9e4ff50f8b..a6ed2e47d5 100644 --- a/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.ts +++ b/src/components/HrTools/PdsGoalCalculator/calculations/usePdsSummaryData.ts @@ -1,4 +1,5 @@ import { useMemo } from 'react'; +import { ApolloError } from '@apollo/client'; import { DesignationSupportFormType } from 'src/graphql/types.generated'; import { useGoalCalculatorConstants } from 'src/hooks/useGoalCalculatorConstants'; import { PdsGoalCalculationFieldsFragment } from '../GoalsList/PdsGoalCalculations.generated'; @@ -32,14 +33,20 @@ export interface PdsSummaryData { geographicMultiplier: number; } +export interface UsePdsSummaryDataResult { + data: PdsSummaryData | null; + loading: boolean; + error: ApolloError | undefined; +} + export const usePdsSummaryData = ( calculation: PdsGoalCalculationFieldsFragment | undefined, hcmUser: HcmUserQuery['hcm'][number] | undefined, -): PdsSummaryData | null => { - const { goalMiscConstants, goalGeographicConstantMap } = +): UsePdsSummaryDataResult => { + const { goalMiscConstants, goalGeographicConstantMap, loading, error } = useGoalCalculatorConstants(); - return useMemo(() => { + const data = useMemo(() => { if (!calculation) { return null; } @@ -85,4 +92,6 @@ export const usePdsSummaryData = ( geographicMultiplier: constants.geographicMultiplier, }; }, [calculation, hcmUser, goalMiscConstants, goalGeographicConstantMap]); + + return { data, loading, error }; }; From ad4add77763f1bde1c689f3d99c9eb6e6a0b9db3 Mon Sep 17 00:00:00 2001 From: Will James Date: Mon, 18 May 2026 11:40:39 -0400 Subject: [PATCH 10/10] Update src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx Co-authored-by: Bizz (Daniel Bisgrove) <56281168+dr-bizz@users.noreply.github.com> --- .../HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx b/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx index b2a8bbdaed..7ff1638eb7 100644 --- a/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx +++ b/src/components/HrTools/PdsGoalCalculator/SupportItem/otherBreakdown.tsx @@ -117,7 +117,7 @@ export const buildOtherBreakdownRows = ( id: 'credit-card-fees', category: t('Credit Card Fees'), formula: t( - '(Subtotal + Attrition) ÷ (1 − {{rate}}) − (Subtotal + Attrition)', + '(Subtotal + Attrition) ÷ (1 - {{rate}}) - (Subtotal + Attrition)', { rate: percentageFormat(constants.creditCardFeeRate, locale), },