Skip to content

[MPDX-9539] Use approval reason from server#1773

Open
canac wants to merge 7 commits into
mainfrom
9539-approval-reason
Open

[MPDX-9539] Use approval reason from server#1773
canac wants to merge 7 commits into
mainfrom
9539-approval-reason

Conversation

@canac
Copy link
Copy Markdown
Contributor

@canac canac commented May 13, 2026

Description

Builds on #1768 and explains why the overlapping requests require MCC approval, even if the request doesn't exceed the user's caps. The API provides the UI with the reason so that approval logic is centralized on the server.

Depends on https://github.com/CruGlobal/mpdx_api/pull/3325

MPDX-9539

Testing

  • ASRs
    • Impersonate caleb.cox@cru.org
    • Open the in-progress ASR
    • Submit the request
    • Check that it says you need MCC approval because you or your spouse as a pending salary request or ASR
Screenshot 2026-05-13 at 12 23 06 PM
  • Salary requests
    • Login to the test account
    • Open the in-progress salary request
    • Go to step 3
    • Check that it says you need MCC approval because you or your spouse as a pending salary request or ASR
Screenshot 2026-05-13 at 12 49 07 PM

Checklist:

  • I have given my PR a title with the format "MPDX-(JIRA#) (summary sentence max 80 chars)"
  • I have applied the appropriate labels (Add the label "Preview" to automatically create a preview environment)
  • I have run the Claude Code /pr-review command locally and fixed any relevant suggestions
  • I have requested a review from another person on the project
  • I have tested my changes in preview or in staging
  • I have cleaned up my commit history

@canac canac self-assigned this May 13, 2026
@canac canac added Preview Environment Add this label to create an Amplify Preview Staging API Run GraphQL codegen against the staging API labels May 13, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Preview branch generated at https://9539-approval-reason.d3dytjb8adxkk5.amplifyapp.com

@github-actions
Copy link
Copy Markdown
Contributor

Bundle sizes [mpdx-react]

Compared against 7222699

No significant changes found

@canac canac force-pushed the 9539-approval-reason branch from 069fadb to 960a3f2 Compare May 13, 2026 18:50
@canac canac force-pushed the 9539-approval-reason branch from 960a3f2 to 4d875b2 Compare May 13, 2026 19:35
Copy link
Copy Markdown
Contributor Author

@canac canac left a comment

Choose a reason for hiding this comment

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

Multi-Agent Code Review — PR #1773

Mode: standard (6 specialized agents + gap review)
Verdict: APPROVED_WITH_SUGGESTIONS
Risk Score: 10/10 — formula-capped; actual blast radius is small (display-logic refactor)

No critical or high-priority blockers. 5 Important findings (severity 7.0–7.9) are strongly recommended before merge — these cannot be dismissed via /dismiss (severity ≥ 7), so please either address them in this PR or open follow-up tickets.

Summary by agent

Agent Critical High Important Medium / Suggestions Confidence
Architecture 0 0 2 3 M
Data Integrity 0 0 2 3 H
Testing 0 0 2 4 H
UX 0 0 2 4 H/M
Standards 0 0 0 0 H — all checklist items PASS
Financial 0 0 1 2 M/H
Gap Review 0 0 0 2

Cross-cutting observations

  • {{approvers}} vs {{approver}} (plural vs singular): useSubmitDialogContent.ts:43 uses {{approvers}} while Receipt.tsx, RequestSummaryCard.tsx, and getCapOverrides.ts use {{approver}} (singular, mapped from progressiveApprovalTier.approver). Likely an inadvertent typo in the new branch.
  • Copy drift across 4 surfaces for the OverlappingRequests message: four subtly different wordings — "Your spouse" vs "You or your spouse", "This will take" vs "This may take", and only getCapOverrides mentions "or Salary Request". Each variant is its own i18n key (cost) and a future drift risk. Consider consolidating into a single canonical sentence (or a small useApprovalReasonCopy() hook) shared across all four surfaces.
  • Server contract dependency: SOSA online-submission block (useSosaBlockOverCap.ts) and several display fallbacks now depend on the server returning the canonical progressiveApprovalTierReason for each scenario. The previous code computed requestedGross > effectiveCap locally and was deterministic. Confirm with the backend team that the enum values are mutually exclusive, that OverUserCap is the canonical reason whenever a user's individual gross exceeds their individual cap (regardless of combined/overlap status), and that the field is always populated when progressiveApprovalTier is set.
  • Dependency impact: clean — no external callers of removed APIs (overUserCap, the old hasBoardCapException shape, additionalApproval param on getCapOverrides).

Findings on Related Files (Not in This PR)

  • src/components/HrTools/AdditionalSalaryRequest/Shared/HcmData/Hcm.graphql:34boardCapException field is now orphaned. Severity 3.5/10 — [Pre-existing]
    • Flagged by: Architecture Agent
    • Recommended action: Consider dropping boardCapException from the Hcm fragment in a follow-up PR; this PR removes the only production consumer (only test mocks still reference it).

This is informational only — it does not block this PR or count toward the verdict.

subContent = t(
"You have a Board approved Maximum Allowable Salary (CAP) and your salary request exceeds that amount. As a result we need to get their approval for this request. We'll forward your request to them and get back to you with their decision.",
);
} else if (reason === ProgressiveApprovalTierReasonEnum.OverlappingRequests) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Important] **Title/body mismatch + plural typo + untested branch.**

Three concerns on this new OverlappingRequests branch:

  1. Title doesn't match the branch. The function returns the same title and content for every non-default case (lines 61–66): "Your request requires additional approval because your Gross Salary exceeds your Maximum Allowable Salary…". That's false for the OverlappingRequests case — the user's salary may be fully within cap; the issue is an overlapping pending request. Users will see contradictory copy in the title vs the subContent.
  2. {{approvers}} vs {{approver}}. This branch uses template var approvers (plural) while the analogous templates in Receipt.tsx, RequestSummaryCard.tsx, and getCapOverrides.ts use approver (singular, mapped from progressiveApprovalTier.approver). Likely a typo; would silently substitute the same singular value either way, but the inconsistency complicates translation.
  3. Untested. useSubmitDialogContent.test.tsx only adds a test for BoardCapException. The new OverlappingRequests branch (and its template substitutions) has no coverage.

Suggested fix: restructure to compute title/content/subContent together inside each if (reason === …) branch (a small lookup or factored helper). Add a colocated test for the OverlappingRequests case asserting both subContent substitution and title correctness.

const isUserSosa =
hcmUser?.staffInfo.userPersonType ===
UserPersonTypeEnum.EmployeeStaffNonRmoSpouse;
const overUserCap =
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Important] **SOSA online-submission safety net now depends on server contract.**

Previously blockOnCap = isUserSosa && (calcs.requestedGross > calcs.effectiveCap) — a deterministic client-side guard that fired whenever the saved gross exceeded the effective cap, by direct comparison. Now it fires only when the server returns progressiveApprovalTierReason === OverUserCap.

This is a behavioral contract change with a real failure mode: if the server classifies a SOSA user's over-cap request with a different reason (e.g. OverCombinedCap for a married SOSA where both individual and combined caps are exceeded, or OverlappingRequests, or returns null transiently), the block disengages and a SOSA user can submit an over-cap request online — exactly the case the block was designed to prevent. The docstring on line 14 still promises "Determine whether a SOSA user's requested gross salary exceeds their effective cap," which the implementation no longer answers.

Recommend one of:

  • Confirm with the backend team that OverUserCap is the canonical reason whenever an individual gross exceeds an individual cap (regardless of other concurrent conditions) — and update the docstring to make the contract dependency explicit.
  • Keep the local requestedGross > effectiveCap check as a defensive belt-and-suspenders fallback (cheap to maintain, prevents silent regression).
  • Treat null reason for a SOSA user with a tier set as "block, unknown state" rather than "allow submission."

Also add a negative-case regression test to useSosaBlockOverCap.test.tsx: SOSA user, numerically over cap, reason is OverCombinedCap (or null) → assert behavior is what you intend (block or pass).

const hasBoardCapException =
hcmUser?.exceptionSalaryCap.boardCapException ?? false;

if (!approvalRequired) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Medium] **Gating still uses legacy `approvalRequired = tier !== DivisionHead`.**

The early-return on line 28 gates the entire branch chain on the legacy boolean. After this PR, RequestSummaryCard.tsx may render a non-trivial approval message for tier === DivisionHead paired with reason === OverUserCap/OverSpouseCap/OverlappingRequests, but the submit dialog will fall through to the generic "Your request will be sent to HR Services" copy. The two surfaces can disagree about whether approval is needed.

Consider gating on reason (or on reason != null && tier) to keep the submit dialog consistent with the status card the user just read.

<CardContent>
<Typography data-testid="ApprovalProcessCard-status">
{tier === ProgressiveApprovalTierEnum.DivisionHead ? (
{tier === ProgressiveApprovalTierEnum.DivisionHead &&
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[Suggestion] **Tightened guard creates an untested branch with potentially contradictory copy.**

The guard changed from tier === DivisionHead ? to tier === DivisionHead && overCapPerson ? — a sound defensive improvement, but the case tier === DivisionHead && !overCapPerson now falls through to the spouseName ? … : … branches. Combined with the second Typography below ("Since your combined request is still within your combined Max Allowable Salary, no additional approvals are required"), a user could see one paragraph saying approval is required and another saying it isn't.

This combination only occurs if the server can return tier === DivisionHead with a reason other than OverUserCap/OverSpouseCap (e.g. OverCombinedCap). If the backend invariant is "DivisionHead ⇒ reason ∈ {OverUserCap, OverSpouseCap}", this is defensive-only and worth a code comment. If not, align the second Typography to also key off reason.

Either way, add a test for tier: DivisionHead with reason: OverCombinedCap (or reason: null).

Comment thread src/components/HrTools/SalaryCalculator/Receipt/Receipt.tsx
@canac canac removed the Staging API Run GraphQL codegen against the staging API label May 13, 2026
@canac canac force-pushed the 9539-approval-reason branch from 464f4d9 to fde3e82 Compare May 13, 2026 20:17
@canac canac requested a review from wjames111 May 14, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Preview Environment Add this label to create an Amplify Preview

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant