Conversation
Restrict the MPD Goal Calculator to SupportedRmo staff and the PDS Goal Calculator to Designation staff. Hide each nav item and show the limited access page for users outside their respective group. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundle sizes [mpdx-react]Compared against 01f7baf
|
wjames111
left a comment
There was a problem hiding this comment.
Multi-Agent Code Review
Verdict: APPROVED WITH SUGGESTIONS — no merge blockers. Several Important and Medium findings recommended for follow-up.
Risk: HIGH (7/10) — modifies a shared gating component (UserTypeAccess, 15 dependents) and a hook consumed by multiple HR Tools pages.
Agents: Security · Architecture · Testing · UX · Standards (debated, with rebuttals)
Top 3 Findings
- PDS detail page is not gated at all (severity 7.5, files NOT in this PR diff — see "Related Files" section)
- Null vs undefined
peopleGroupSupportTypecreates fail-open / asymmetric behavior (severity 7.0, useUsStaffGroups.ts:58) - MPD/PDS goal-calc nav items flash visible during HCM load (severity 6.5, useHrToolsNavItems.ts)
These are not blockers — the PR's stated objective (gating list pages by support type) is correctly implemented and tested. Findings are improvements to make the gating consistent and robust.
Cross-Cutting Consistency Check — Detail-Page Gating Gap
The PR title commits to gating MPD and PDS Goal Calculators by peopleGroupSupportType. Verified the gate is applied at the list page level only:
| Page | Auth | Support-type gate |
|---|---|---|
goalCalculator/index.page.tsx (list) |
UsStaff | ✅ MpdGoalCalc |
pdsGoalCalculator/index.page.tsx (list) |
UsStaff | ✅ PdsGoalCalc |
goalCalculator/[goalCalculationId].page.tsx (detail) |
UsStaff (no requireUserGroups) |
❌ MISSING |
pdsGoalCalculator/[pdsGoalId].page.tsx (detail) |
❌ NO UserTypeAccess wrapper |
❌ MISSING |
For comparison, salaryCalculator/[calculationId]/index.page.tsx:62 and mhaCalculator/[requestId].page.tsx:69 both pass requireUserGroups={...} to their detail page wrappers — that pattern is established in this repo. A user with an ineligible peopleGroupSupportType who has a valid goalCalculationId / pdsGoalId (bookmark, shared link, browser history) can deep-link directly to the detail page and bypass the new gate. Server-side authorization on the upstream MPDX API should be the ultimate defense, but UI parity with the existing pattern is the highest-leverage UI fix.
Findings on Related Files (Not in This PR)
These files were identified as having gating-gap issues directly caused by this PR's design choices, but the files themselves are not modified by this PR. They cannot be posted as line comments.
[Important] pages/accountLists/[accountListId]/hrTools/pdsGoalCalculator/[pdsGoalId].page.tsx — No UserTypeAccess wrapper at all
- Severity: 7.5/10
- Flagged by: Architecture, Security
- Problem: The PDS detail page has no
UserTypeAccesswrapper. After this PR ships, an ineligible US Staff user (or even a non-US-Staff user) can deep-link apdsGoalIdand reach the detail page directly. - Recommended Action: Wrap with
<UserTypeAccess requireUserGroups={RequiredUserGroupEnum.PdsGoalCalc}>to match the new list-page gating.
[Medium] pages/accountLists/[accountListId]/hrTools/goalCalculator/[goalCalculationId].page.tsx:142 — UserTypeAccess is missing requireUserGroups
- Severity: 6.5/10
- Flagged by: Architecture, Security
- Problem: The MPD detail page wraps in
<UserTypeAccess>with norequireUserGroups— so it only checksuserType === UsStaff, not the support-type gate. Pre-existing inconsistency, but in scope to fix given this PR's stated goal. - Recommended Action: Change to
<UserTypeAccess requireUserGroups={RequiredUserGroupEnum.MpdGoalCalc}>.
Other Notes (Not Inline)
[Suggestion] Server-side enforcement of peopleGroupSupportType is unverified (severity 6.5, Security)
UserTypeAccessis a UI-only gate. Per.claude/rules/code-review.mdSecurity Focus Areas: "every Yup rule, every disabled button, every required field must have a server-side equivalent." The upstream MPDX GraphQL API forgoalCalculations/designationSupportCalculationsand their mutations should enforcepeopleGroupSupportTypeindependently. Not visible in this repo — verify with the API team. Same posture already exists for ASR/Salary/MHA, so this is not a regression.
[Suggestion] LimitedAccess copy is misleading when peopleGroupSupportType is null/None (severity 4.5, UX, pre-existing)
- A US Staff user whose
peopleGroupSupportTypeis genuinelyNoneornullsees "Our records show that you are not part of the user group… contact support@mpdx.org to change your user group" (src/components/Shared/LimitedAccess/getLimitedText.tsx:42-51). The actionable fix is with HR/HCM data ownership, not MPDX support. Pre-existing copy.
[Suggestion] Senior staff landing on PDS page has no signposting to MPD calc (severity 5.5, UX)
- A
SupportedRmouser navigating to PDS (orDesignationuser to MPD) hits a generic "limited access" wall with only a "Back to Dashboard" CTA. The nav menu correctly hides the wrong calculator, so they have a way forward — but the page itself doesn't signpost. Out of scope for this PR.
[Pre-existing] PdsGoalCalculatorPage uses export default (severity 2.0, Standards)
- Next.js Pages Router requires default export. This is a project-wide carve-out from CLAUDE.md's "named exports only." Not introduced by this PR.
Standards Compliance — All Pass
- Named exports for new code, file naming, i18n via
t(), generated GraphQL types used, typedGqlMockedProvider,findBy*for async assertions — all PASS. yarn lint:ciandyarn lint:tsclean on changed files.- No
any, no debug output, nonew Date(), no commented code.
To dismiss a non-blocking suggestion (severity < 7), reply /dismiss: <reason> on the comment.
| }); | ||
| }); | ||
|
|
||
| describe('Goal Calculator eligibility', () => { |
There was a problem hiding this comment.
The new Goal Calculator eligibility describe block covers SupportedRmo, Designation, SupportedNonRmo, None, and a spouse case — but not the two nullish cases that drive the asymmetric fail-open behavior (see comment on useUsStaffGroups.ts:58):
peopleGroupSupportType: undefined(field omitted) — currently treated as eligible for both calcs (fail-open)peopleGroupSupportType: null(explicit null from server) — currently treated as ineligible for both calcs (fail-closed)
Whichever contract the team intends, add tests to lock it in. UX recommends the test also assert the rendered body copy of LimitedAccess so the misleading "contact support@mpdx.org" message doesn't silently survive a refactor.
Suggested additions:
it('marks user as ineligible for both goal calcs when peopleGroupSupportType is null', async () => {
const { result } = renderUseUsStaffGroups(
buildHcmMock({
peopleGroupSupportType: null as unknown as PeopleGroupSupportTypeEnum,
}),
);
await waitFor(() => {
expect(result.current.inMpdGoalCalcIneligibleGroup).toBe(true);
expect(result.current.inPdsGoalCalcIneligibleGroup).toBe(true);
});
});
it('treats peopleGroupSupportType undefined as <chosen behavior>', async () => {
const { result } = renderUseUsStaffGroups({
hcm: [{ asrEit: { asrEligibility: true }, salaryRequestEligible: true, mhaEit: { mhaEligibility: true }, staffInfo: { /* peopleGroupSupportType intentionally omitted */ } }],
} as HcmQuery);
await waitFor(() => {
// Pin to the chosen behavior
});
});Note: Do not write tests against the current fail-open behavior if you plan to also adopt the architectural fix on useUsStaffGroups.ts:58 — write tests against the fixed behavior.
There was a problem hiding this comment.
Not sure what this should default to.
|
Preview branch generated at https://MPDX-9613.d3dytjb8adxkk5.amplifyapp.com |
kegrimes
left a comment
There was a problem hiding this comment.
Thank you for doing this Will, it looks amazing! I did have a few suggestions and found a couple bugs.
Also just to let you know, I have already updated the backend to cache all possible US staff groups (senior staff, new staff, PTFS, paid with designation, and interns) into the User record. This will eliminate the extra call we are making to HCM so, a lot of this work will be refactored! But, thank you for adding MPD and PDS calculators to the list! That is one less thing I need to do.
Would you mind adding MPD and PDS goal calculators to this document with the groups who are eligible for them? This is just so we can keep track of who is eligible for what!
https://docs.google.com/document/d/1OWkY1iDCE-DGlVuYMYpLcvtPucmjD-j1wQt9AsQxxIs/edit?tab=t.0
| const inMpdGoalCalcIneligibleGroup = | ||
| hasNoStaffAccount || | ||
| userSupportType !== PeopleGroupSupportTypeEnum.SupportedRmo; |
There was a problem hiding this comment.
If senior staff are the only ones who are eligible for MPD goal calculator, I don't think this check is correct. The status codes for senior staff are...
- SC - Staff Full time
Employee - Staff, Supported - RMO, Active - Payroll Eligible, Full-time regular - SCO - Staff Full time only
Employee - Staff non-RMO Spouse, Supported - RMO, Active - Payroll Eligible, Full-time regular
...but an intern can also be Supported - RMO (if you impersonate nataley.guetherman@cru.org from this excel sheet, you will be able to see MPD)
For MHA, ASR, and Salary calc, I have the eligibility validators checking the fields instead of doing it like this on the frontend. Is there an eligibility field for MPD (and even PDS)? If there is, I would use that field instead and verify that it is for a senior staff only (you can check Salary calc in the backend for reference - senior staff only).
But that might need to be another PR for the backend if those eligibility checks are not correct for MPD and PDS. If that is the case, then I would keep these checks on the frontend but fix senior staff to only be those status codes listed above. Senior staff is the one group that needs all 4 fields to check. You can look at the status codes here to verify)
|
@wjames111 Now that I am thinking about it, I don't think the PDS calculator requires a staff account id because paid with designation staff members do not have one at all. |
|
@kegrimes Okay so I made the changes you requested. Also |
kegrimes
left a comment
There was a problem hiding this comment.
We are almost there! Just a couple things. I want to be able to test the HR Tools tab not showing up when the user does not have access to any of those reports! The new Alithya change affected that, so we need to change that.
|
I also reached out to Daniel to see if he could merge his PR to unblock us for the backend work! [Update]: It has been merged to production! |
kegrimes
left a comment
There was a problem hiding this comment.
Great work Will!
I have one more bug fix for you, but other than that, this PR is good to go so I am going to approve. Thank you for all your hard work! It will be easier for me to update the frontend customized work with MPD and PDS set up!
| hasNoHcmData || user?.salaryRequestEligible === false; | ||
| const inMhaIneligibleGroup = hasNoHcmData || allMhaIneligible; | ||
| const inMpdGoalCalcIneligibleGroup = user?.salaryRequestEligible === false; | ||
| const inPdsGoalCalcIneligibleGroup = hasNoHcmData || allPdsGoalCalcIneligible; |
There was a problem hiding this comment.
I actually think we need to separate these errors. I was impersonating myself and it was still allowing me to see the MPD goal calculator even though I did not have any HCM data. I think we need to do something like this:
const hasNoStaffAccount =
error?.graphQLErrors.some(
(graphQLError) => graphQLError.extensions?.code === 'NO_STAFF_ACCOUNT',
) ?? false;
const hcmPersonNotFound =
error?.graphQLErrors.some(
(graphQLError) =>
graphQLError.extensions?.code === 'HCM_PERSON_NOT_FOUND',
) ?? false;
const hasNoHcmData = hasNoStaffAccount || hcmPersonNotFound;
const inAsrIneligibleGroup = hasNoHcmData || allAsrIneligible;
const inSalaryCalcIneligibleGroup =
hasNoHcmData || user?.salaryRequestEligible === false;
const inMhaIneligibleGroup = hasNoHcmData || allMhaIneligible;
const inMpdGoalCalcIneligibleGroup =
hcmPersonNotFound || user?.salaryRequestEligible === false;
const inPdsGoalCalcIneligibleGroup =
hcmPersonNotFound || allPdsGoalCalcIneligible;
I already tested all 5 scenarios and they should all be working with this logic. MPD and PDS calculators don't require a staff account id, so even without one HCM still gets called (we have a person number) and can come back with HCM_PERSON_NOT_FOUND. We only want to check that error against these 2 calculators. NO_STAFF_ACCOUNT just means no account id exists, and since these reports don't require one, we don't want that error to hide them. ASR, Salary, and MHA do require a staff account id, so they hide on either error (hasNoHcmData).
So the distinction is:
NO_STAFF_ACCOUNT= no id exists (raised before HCM hit)HCM_PERSON_NOT_FOUND= HCM data was reached, but there is no data
There was a problem hiding this comment.
If you are wanting to test these for yourself, kathryn.hougham@cru.org is the only account that will work for paid with designation users. She has her person number set up correctly in staging!

Description
peopleGroupSupportTypeisSupportedRmo(senior staff).peopleGroupSupportTypeisDesignation.MpdGoalCalcandPdsGoalCalcvalues toRequiredUserGroupEnumand extenduseUsStaffGroupswith matchinginMpdGoalCalcIneligibleGroup/inPdsGoalCalcIneligibleGroupflags.UserTypeAccessso ineligible users see the "Access to this feature is limited." page.Jira: MPDX-9613
Testing
peopleGroupSupportTypeisSupportedRmopeopleGroupSupportTypeisDesignationChecklist:
/pr-reviewcommand locally and fixed any relevant suggestions