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 @@ -4,13 +4,20 @@ query LandingSalaryCalculations {
) {
id
status
personNumber
salary
spouseSalary
mhaAmount
spouseMhaAmount
submittedAt
changesRequestedAt
feedback
calculations {
requestedGross
}
spouseCalculations {
requestedGross
}
}
inProgressCalculation: latestSalaryRequest(status: [IN_PROGRESS]) {
id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
hasApprovedCalculation?: boolean;
hasSpouseApprovedCalculation?: boolean;
hasLatestCalculation?: boolean;
hasSpouseLatestCalculation?: boolean;
salaryRequestEligible?: boolean;
}

Expand All @@ -36,89 +37,97 @@
hasApprovedCalculation = false,
hasSpouseApprovedCalculation = false,
hasLatestCalculation = false,
hasSpouseLatestCalculation = false,
salaryRequestEligible = true,
}) => (
<ThemeProvider theme={theme}>
<TestRouter>
<GqlMockedProvider<{
Hcm: HcmQuery;
StaffAccountId: StaffAccountIdQuery;
AccountBalance: AccountBalanceQuery;
LandingSalaryCalculations: LandingSalaryCalculationsQuery;
GetUser: GetUserQuery;
}>
mocks={{
Hcm: {
hcm: [
{
staffInfo: {
preferredName: 'John',
lastName: 'Doe',
personNumber: '000123456',
secaStatus: SecaStatusEnum.Seca,
peopleGroupSupportType:
PeopleGroupSupportTypeEnum.SupportedRmo,
assignmentStatus: AssignmentStatusEnum.ActivePayrollEligible,
assignmentCategory: AssignmentCategoryEnum.FullTimeRegular,
userPersonType: UserPersonTypeEnum.EmployeeStaff,
},
currentSalary: {
grossSalaryAmount: 55000,
lastUpdated: '2024-03-01',
},
fourOThreeB: {
currentRothContributionPercentage: 12,
currentTaxDeferredContributionPercentage: 5,
},
mhaRequest: {
currentTakenAmount: 10000,
},
salaryRequestEligible,
},
{
staffInfo: {
preferredName: 'Jane',
lastName: 'Doe',
personNumber: '000123457',
secaStatus: SecaStatusEnum.Seca,
},
currentSalary: {
grossSalaryAmount: 10000,
lastUpdated: '2024-03-01',
},
fourOThreeB: {
currentRothContributionPercentage: 10,
currentTaxDeferredContributionPercentage: 6,
},
mhaRequest: {
currentTakenAmount: 12000,
},
salaryRequestEligible,
},
],
},
LandingSalaryCalculations: {
inProgressCalculation: hasInProgressCalculation
? { id: 'in-progress-calc-1' }
: null,
effectiveCalculation: hasApprovedCalculation
? {
personNumber: hasSpouseApprovedCalculation
? '000123457'
: '000123456',
salary: 50000,
spouseSalary: 60000,
calculations: { effectiveCap: 60000 },
spouseCalculations: { effectiveCap: 70000 },
}
: null,
latestCalculation: hasLatestCalculation
? {
id: 'pending-calc-1',
status: SalaryRequestStatusEnum.Pending,
personNumber: hasSpouseLatestCalculation
? '000123457'
: '000123456',
salary: 52000,
spouseSalary: 51000,
submittedAt: '2025-01-16T10:00:00Z',
changesRequestedAt: null,
feedback: null,
calculations: { requestedGross: 69714.29 },
spouseCalculations: { requestedGross: 62000 },

Check warning on line 130 in src/components/HrTools/SalaryCalculator/Landing/NewSalaryCalculationLanding/LandingTestWrapper.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Large Method

LandingTestWrapper:React.FC<LandingTestWrapperProps> has 125 lines, threshold = 120 Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
}
: null,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React from 'react';
import { render } from '@testing-library/react';
import { LandingTestWrapper } from '../NewSalaryCalculationLanding/LandingTestWrapper';
import { render, waitFor } from '@testing-library/react';
import {
LandingTestWrapper,
LandingTestWrapperProps,
} from '../NewSalaryCalculationLanding/LandingTestWrapper';
import { PendingRequestCard } from './PendingRequestCard';

const TestComponent: React.FC = () => (
<LandingTestWrapper hasLatestCalculation>
const TestComponent: React.FC<LandingTestWrapperProps> = (props) => (
<LandingTestWrapper hasLatestCalculation {...props}>
<PendingRequestCard />
</LandingTestWrapper>
);
Expand All @@ -19,6 +22,57 @@ describe('PendingRequestCard', () => {
).toBeInTheDocument();
});

it('renders the requested gross salary, not the current gross salary', async () => {
const { getByTestId } = render(<TestComponent />);

await waitFor(() =>
expect(getByTestId('gross-salary-amount')).toHaveTextContent(
'$69,714.29',
),
);
});

it("renders the spouse's requested gross salary alongside the user's", async () => {
const { getByTestId, getByText } = render(<TestComponent />);

await waitFor(() =>
expect(getByTestId('spouse-gross-salary-amount')).toHaveTextContent(
'$62,000.00',
),
);
expect(getByText('John')).toBeInTheDocument();
expect(getByText('Jane')).toBeInTheDocument();
});

it('swaps the amounts when the spouse created the request', async () => {
const { getByTestId } = render(
<TestComponent hasSpouseLatestCalculation />,
);

await waitFor(() =>
expect(getByTestId('gross-salary-amount')).toHaveTextContent(
'$62,000.00',
),
);
expect(getByTestId('spouse-gross-salary-amount')).toHaveTextContent(
'$69,714.29',
);
});

it('renders a single amount and no names when there is no spouse', async () => {
const { getByTestId, queryByTestId, queryByText } = render(
<TestComponent salaryRequestEligible={false} />,
);

await waitFor(() =>
expect(getByTestId('gross-salary-amount')).toHaveTextContent(
'$69,714.29',
),
);
expect(queryByTestId('spouse-gross-salary-amount')).not.toBeInTheDocument();
expect(queryByText('John')).not.toBeInTheDocument();
});

it('renders print link with correct href', async () => {
const { findByRole } = render(<TestComponent />);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Card,
CardContent,
CardHeader,
Grid,
IconButton,
Typography,
} from '@mui/material';
Expand All @@ -21,16 +22,19 @@ import { PendingRequestTimeline } from './components/PendingRequestTimeline';
export const PendingRequestCard: React.FC = () => {
const { t } = useTranslation();
const accountListId = useAccountListId();
const {
calculation,
requestedOn,
processedOn,
feedback,
salaryData: { currentGrossSalary },
} = useLandingData();
const { calculation, self, spouse, requestedOn, processedOn, feedback } =
useLandingData();

const locale = useLocale();

// The gross request is the requested salary plus SECA and 403(b) contributions
const requestedGross = calculation?.calculations?.requestedGross ?? 0;
const spouseRequestedGross = calculation?.spouseCalculations?.requestedGross;
const hasSpouse = !!spouse && spouseRequestedGross !== undefined;

const formatGross = (amount: number) =>
currencyFormat(amount, 'USD', locale, { showTrailingZeros: true });

return (
<Card sx={{ marginBlock: theme.spacing(3) }}>
<CardHeader
Expand Down Expand Up @@ -61,16 +65,38 @@ export const PendingRequestCard: React.FC = () => {
>
{t('Gross Salary Requested')?.toUpperCase()}
</Typography>
<Typography
variant="h3"
fontWeight="bold"
sx={{ color: 'primary.main' }}
data-testid="gross-salary-amount"
>
{currencyFormat(currentGrossSalary, 'USD', locale, {
showTrailingZeros: true,
})}
</Typography>
<Grid container spacing={theme.spacing(2)}>
<Grid size={hasSpouse ? { xs: 12, md: 6 } : 12}>
{hasSpouse && (
<Typography variant="body2" color="textSecondary">
{self?.staffInfo.preferredName}
</Typography>
)}
<Typography
variant="h3"
fontWeight="bold"
sx={{ color: 'primary.main' }}
data-testid="gross-salary-amount"
>
{formatGross(requestedGross)}
</Typography>
</Grid>
{hasSpouse && (
<Grid size={{ xs: 12, md: 6 }}>
<Typography variant="body2" color="textSecondary">
{spouse.staffInfo.preferredName}
</Typography>
<Typography
variant="h3"
fontWeight="bold"
sx={{ color: 'primary.main' }}
data-testid="spouse-gross-salary-amount"
>
{formatGross(spouseRequestedGross)}
</Typography>
</Grid>
)}
</Grid>
<PendingRequestTimeline
calculation={calculation}
requestedOn={requestedOn}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type LatestCalculation = LandingSalaryCalculationsQuery['latestCalculation'];

const mockCalculation: LatestCalculation = {
id: '1',
personNumber: '000123456',
mhaAmount: null,
spouseMhaAmount: null,
salary: null,
Expand All @@ -17,6 +18,8 @@ const mockCalculation: LatestCalculation = {
changesRequestedAt: null,
feedback: null,
status: SalaryRequestStatusEnum.Pending,
calculations: { requestedGross: 0 },
spouseCalculations: null,
};

const mutationSpy = jest.fn();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ const createCalculation = (
): LatestCalculation => ({
id: '1',
status,
personNumber: '000123456',
mhaAmount: null,
spouseMhaAmount: null,
salary: null,
spouseSalary: null,
submittedAt: '2025-01-15T10:00:00Z',
changesRequestedAt: null,
feedback,
calculations: { requestedGross: 0 },
spouseCalculations: null,
});

interface TestComponentProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,175 +96,182 @@
};
}, [hcmData]);

// The latest calculation may have been created by the spouse, so orient it to the user's
// perspective before exposing its amounts
const calculation = useMemo(
() => orientSalaryRequest(latestCalculation, self?.staffInfo.personNumber),
[latestCalculation, self],
);

const staffAccountId = useMemo(
() => staffAccountIdData?.user?.staffAccountId ?? null,
[staffAccountIdData],
);

const names = useMemo(() => {
if (!self?.staffInfo?.preferredName || !self?.staffInfo?.lastName) {
return '';
}
const selfName = `${self.staffInfo.lastName}, ${self.staffInfo.preferredName}`;
if (!spouse || !spouse?.staffInfo?.preferredName) {
return selfName;
}
return `${selfName} and ${spouse.staffInfo.preferredName}`;
}, [self, spouse]);

const salaryData = useMemo((): SalaryData => {
const currentGrossSalary = self?.currentSalary.grossSalaryAmount ?? 0;
const lastUpdated = self?.currentSalary.lastUpdated ?? '';
const spouseCurrentGrossSalary =
spouse?.currentSalary.grossSalaryAmount ?? 0;

const rothContribution =
self?.fourOThreeB.currentRothContributionPercentage ?? 0;
const spouseRothContribution =
spouse?.fourOThreeB.currentRothContributionPercentage ?? 0;

const taxDeferredContribution =
self?.fourOThreeB.currentTaxDeferredContributionPercentage ?? 0;
const spouseTaxDeferredContribution =
spouse?.fourOThreeB.currentTaxDeferredContributionPercentage ?? 0;

const takenMha = self?.mhaRequest.currentTakenAmount ?? 0;
const spouseTakenMha = spouse?.mhaRequest.currentTakenAmount ?? 0;

return {
currentGrossSalary,
lastUpdated,
spouseCurrentGrossSalary,
rothContribution,
spouseRothContribution,
taxDeferredContribution,
spouseTaxDeferredContribution,
takenMha,
spouseTakenMha,
};
}, [self, spouse]);

const accountBalance = useMemo(
() =>
accountBalanceData?.reportsStaffExpenses?.funds?.reduce(
(sum, fund) => sum + (fund.total ?? 0),
0,
) ?? 0,
[accountBalanceData],
);

const salaryCategories = useMemo<SalaryCategory[]>(() => {
const effectiveCalculation = orientSalaryRequest(
calculationData?.effectiveCalculation,
self?.staffInfo.personNumber,
);

return [
{
category: t('Maximum Allowable Salary'),
user: effectiveCalculation?.calculations.effectiveCap
? currencyFormat(
effectiveCalculation.calculations.effectiveCap,
'USD',
locale,
{ showTrailingZeros: true },
)
: 'TBD',
spouse: effectiveCalculation?.spouseCalculations?.effectiveCap
? currencyFormat(
effectiveCalculation.spouseCalculations?.effectiveCap,
'USD',
locale,
{ showTrailingZeros: true },
)
: 'TBD',
},
{
category: t('Requested Salary'),
user: effectiveCalculation?.salary
? currencyFormat(effectiveCalculation.salary, 'USD', locale, {
showTrailingZeros: true,
})
: 'TBD',
spouse: effectiveCalculation?.spouseSalary
? currencyFormat(effectiveCalculation.spouseSalary, 'USD', locale, {
showTrailingZeros: true,
})
: 'TBD',
tooltip: t(
'Requested Salary includes MHA and taxes if applicable. It does not include 403(b) Contributions and SECA.',
),
},
{
category: t('Tax-deferred 403(b) Contribution'),
user: salaryData.taxDeferredContribution
? percentageFormat(salaryData.taxDeferredContribution / 100, locale)
: '-',
spouse: salaryData.spouseTaxDeferredContribution
? percentageFormat(
salaryData.spouseTaxDeferredContribution / 100,
locale,
)
: '-',
},
{
category: t('Roth 403(b) Contribution'),
user: salaryData.rothContribution
? percentageFormat(salaryData.rothContribution / 100, locale)
: '-',
spouse: salaryData.spouseRothContribution
? percentageFormat(salaryData.spouseRothContribution / 100, locale)
: '-',
},
{
category: t('Security (SECA/FICA) Status'),
user: getLocalizedTaxStatus(self?.staffInfo.secaStatus, t),
spouse: getLocalizedTaxStatus(spouse?.staffInfo.secaStatus, t),
},
{
category: t('Current Gross Salary'),
user: salaryData.currentGrossSalary
? currencyFormat(salaryData.currentGrossSalary, 'USD', locale, {
showTrailingZeros: true,
})
: '-',
spouse: salaryData.spouseCurrentGrossSalary
? currencyFormat(salaryData.spouseCurrentGrossSalary, 'USD', locale, {
showTrailingZeros: true,
})
: '-',
tooltip: t(
'Current Gross Salary includes MHA, 403(b), SECA, and taxes if applicable.',
),
},
{
category: t('Current MHA (Included in Current Gross Salary)'),
user: currencyFormat(salaryData.takenMha, 'USD', locale, {
showTrailingZeros: true,
}),
spouse: currencyFormat(salaryData.spouseTakenMha, 'USD', locale, {
showTrailingZeros: true,
}),
link: '/hrTools/mhaCalculator',
},
];
}, [t, salaryData, self, spouse, calculationData, locale]);

return {
staffAccountId,
names,
self,
spouse,
salaryData,
salaryCategories,
accountBalance,
inProgressCalculationId,
loading:
hcmLoading ||
calculationLoading ||
accountBalanceLoading ||
staffAccountIdLoading,
calculation: latestCalculation,
calculation,

Check warning on line 274 in src/components/HrTools/SalaryCalculator/Landing/useLandingData.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Complex Method

useLandingData increases in cyclomatic complexity from 58 to 59, threshold = 20 This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
requestedOn,
processedOn,
feedback,
Expand Down
Loading