Skip to content

MPDX-9818 - Add DA5 and A33 warnings/errors to goal settings#1930

Open
zweatshirt wants to merge 4 commits into
mainfrom
MPDX-9818
Open

MPDX-9818 - Add DA5 and A33 warnings/errors to goal settings#1930
zweatshirt wants to merge 4 commits into
mainfrom
MPDX-9818

Conversation

@zweatshirt

@zweatshirt zweatshirt commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Description

We want to show a warning to the admin when the salary the admin has entered is over the salary cap.
Backend PR: https://github.com/CruGlobal/mpdx_api/pull/3469

Testing

  • Go to the GoalSettingsForm
  • Enter a large amount for 'Annual Requested Salary'
  • Check that the warning displays that the amount exceeds the salary cap.

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

@zweatshirt zweatshirt self-assigned this Jul 20, 2026
@zweatshirt zweatshirt added Preview Environment Add this label to create an Amplify Preview Staging API Run GraphQL codegen against the staging API labels Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Preview branch generated at https://MPDX-9818.d3dytjb8adxkk5.amplifyapp.com

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Bundle sizes [mpdx-react]

Compared against ea57016

Route Size (gzipped) Diff
/accountLists/[accountListId]/coaching/[coachingId]/nsGoalCalculator 243.45 KB +1.08 KB
/accountLists/[accountListId]/hrTools/mpdGoalAdmin/scenario/[scenarioGoalId] 243.44 KB +1.08 KB

@zweatshirt zweatshirt removed the Staging API Run GraphQL codegen against the staging API label Jul 21, 2026

@zweatshirt zweatshirt left a comment

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.

Multi-Agent Code Review — PR #1930

Verdict: APPROVED WITH SUGGESTIONS — no blockers. Highest finding is 6.5/10.

Risk: 7/10 (HIGH)src/components/**/*.graphql (+2), 4x feature components (+4), 146 non-test lines (+1). Senior review recommended. Codegen verified clean (yarn gql succeeds, yarn lint:ts exits 0, 213 tests pass).

Two rounds: 6 specialized agents each. Round 2 was re-run with round-1 findings adjudicated against verified facts, which recalibrated three findings downward and surfaced two better ones.

Key recalibrations from round 1

Finding Was Now Why
Failed preview silently suppresses warning 7.5 3.5 Self-corrects on save (UpdateNewStaffGoalCalculation returns the fragment incl. salaryOverCap; enableReinitialize set; Save never gated on it)
Duplicate preview mutations 7.5 6.5 BatchHttpLink (batchInterval 20ms) coalesces both into one HTTP request; cost is doubled server compute, not round-trips
savedSalaryOverCap = false fails open 4.5 2.0 The only caller omitting it (MpdGoalPreview) never reads salaryOverCap

Verified correct (no action)

  • The always-mounted empty aria-live region is proper live-region technique — mutation-tested: deleting aria-live and making the region conditional both fail the suite.
  • Cache normalization correct (NewStaffGoalCalculationCalculations has no id; non-normalizable value type). fetchPolicy: 'no-cache' correct (NewStaffGoalCalculationPreview is a distinct typename).
  • Destructuring-with-defaults change is behaviourally identical (monthlyGoal: Float!, salaryOverCap: Boolean!).
  • Scenario-goal path (accountListId: null) is valid per schema and pre-existing via MpdGoalPreview.
  • Save-while-preview-in-flight race traced and safe.
  • Cap semantics: a non-joining senior-staff spouse's salary IS included against the married cap (applicable? = !spouse? || !single?) — coherent, no bug. SOSA correctly treated as single.
  • Mock fixtures deterministic (seed = 'seed', probed across processes).
  • salaryOverCap: false added to MpdGoalPreview.test.tsx is inert — removing it still passes 15/15.
  • Missing public/locales/en key follows the repo's batch-extraction convention. Not a violation.

Findings outside this PR's diff (informational, not blockers)

1. The contrast fix belongs in src/theme.ts, not in this component. src/theme.ts never defines palette.warning, so warning.main resolves to MUI's default #ed6c02 (3.11:1 on #fff). But HrTools uses warning.main in 7 of 7 pre-existing cases — changing only this file would make it the sole diverging file. Systemic fix:

warning: { main: statusColors.warning },  // #D34400 -> 4.57:1
success: { main: statusColors.success },  // CardTitle.tsx:24 has the same latent defect

Pre-existing HrTools-wide. Recommend a standalone ticket rather than blocking this PR.

2. Review-process gap. .claude/rules/code-review.md lists src/components/User/impersonate*, which matches nothing — impersonation lives in Settings/Admin/ImpersonateUser/, Settings/Organization/ImpersonateUser/, and ProfileMenu/. The Security agent's impersonation trigger never fires. Also stale: src/components/Reports/SalaryCalculator/** and pages/api/Schema/uploads/** don't exist.

3. Doc drift. CLAUDE.md and the rules file say "Material UI v5"; yarn.lock resolves @mui/material 7.3.11.

4. Questions for the API team (mpdx_api #3469):

  • salary_over_cap? calls base_salary_cap unconditionally, whereas salary_cap only reaches it when allow_salary_over_cap == 'no'. It resolves via find_by!, which raises. Are the four board_approved_salary_calc misc constants seeded for every historical calculations_year? A raise would null the whole calculations block via non-null propagation, taking monthlyGoal down with it.
  • salary_over_cap? uses strict > — confirm equal-to-cap is intended as compliant.

Recommended order of work

  1. The flicker fix (useMpdGoalPreview.ts:151) — one line, real user-facing defect
  2. Two test assertions (warning renders in the form; accountListId in both directions) — closes a mutation-proven hole
  3. Resolve the dead mocks prop either way
  4. Copy revision
  5. Defer: theme palette, doc drift, rules globs

Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/useMpdGoalPreview.ts Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsForm.tsx Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsWarning.tsx Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsWarning.tsx Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsWarning.test.tsx Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/useMpdGoalPreview.ts Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/useMpdGoalPreview.ts Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/useMpdGoalPreview.ts Outdated
@zweatshirt
zweatshirt marked this pull request as ready for review July 21, 2026 14:55

@zweatshirt zweatshirt left a comment

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.

Multi-Agent Code Review — PR #1930 (Round 3)

Verdict: APPROVED WITH SUGGESTIONS — no blockers. Highest finding 6.0/10.

Risk: 7/10 (HIGH)src/components/**/*.graphql (+2), 4x feature components (+4), ~145 non-test lines (+1). Senior review recommended.

6 agents (Architecture, Testing, Standards, Data Integrity, UX, Financial Reporting). Third round on this branch; the six fix-up commits since the last review closed every prior finding, so this pass focuses on the new code.

Confirmed fixed since the last review

Verified by mutation testing — each item below now fails a targeted test when reverted:

Fix Guarded by
Warning flicker on every edit does not re-raise a resolved warning…
accountListId untested in both directions both useNewStaffGoalCalculation tests
Warning-in-form integration unasserted shows the over-cap warning beside the save actions
Real timers against a 500ms debounce fake timers added; probed with a 5,000,000ms debounce
Dead mocks test prop removed
Stale plural JSDoc, unused export fixed

Mutation audit: 9 of 10 mutations caught. The one gap is the .catch fallback — see the inline comment on useMpdGoalPreview.ts.

Root cause found for "the warning shows when Annual Requested Salary is empty"

This was reported during review and is not stale persistence. app/services/new_staff_goals/salary_calculation.rb:73-81:

requested_annual_salary = calculator_attribute(:requested_annual_salary)&.to_d
requested_annual_salary || ((geographic_multiplier * base) + tenure_adjustment + debt_salary)

An empty field maps to null on the client (toNumberOrNull('')), and the API then computes a default salary and compares that against the cap. The flag is correct — the goal calculation really does use that computed figure. The copy is what's wrong: it says "requested salary" when nothing was requested. See the inline comment on GoalSettingsWarning.tsx.

Findings on related files (not in this PR)

1. Contrast fix belongs in src/theme.ts, not in the component. palette.warning is never defined, so warning.main resolves to MUI's default #ed6c023.11:1 on #fff, failing the 4.5:1 bar for 14px body2. The repo's own statusWarning (#D34400) is 4.57:1. But HrTools uses warning.main in 7 of 7 pre-existing cases, so changing only this file would make it the sole outlier. Systemic fix:

warning: { main: statusColors.warning },  // #D34400 -> 4.57:1
success: { main: statusColors.success },  // CardTitle.tsx:24 has the same latent defect

Pre-existing and tree-wide — should not block this PR. Standalone ticket recommended.

2. Review-process gap. .claude/rules/code-review.md lists src/components/User/impersonate*, which matches nothing — impersonation lives under Settings/Admin/ImpersonateUser/, Settings/Organization/ImpersonateUser/, and ProfileMenu/. The Security agent's impersonation trigger never fires. Also stale: src/components/Reports/SalaryCalculator/** and pages/api/Schema/uploads/**.

3. Doc drift. CLAUDE.md and the rules file say "Material UI v5"; yarn.lock resolves @mui/material 7.3.11.

Questions for the API team (mpdx_api #3469)

  1. Expose the amounts behind the warning. To say "Total salary of $X is over the standard cap of $Y" the client needs combined_uncapped_total_salary and base_salary_cap on NewStaffGoalCalculationCalculations. Neither is exposed today, and the three salary fields that are (salary, salarySubtotal, totalSalary) are all monthly and post-cappingbase_salary scales down when the combined total exceeds the cap, so totalSalary is by construction never over the cap. Rendering any of them beside "is over the cap" would show a number that isn't. This is the blocker on improving the copy further.
  2. Seeding. salary_over_cap? calls base_salary_cap unconditionally, whereas salary_cap only reaches it when allow_salary_over_cap == 'no'. It resolves via MpdGoalMiscConstant.find_by!, which raises, and a raise would null the whole calculations block via non-null propagation — taking monthlyGoal with it. The in-repo seeder is all-or-nothing per year (so the risk is pre-existing, not new), but production seeding isn't in the repo. Is the prod path also all-or-nothing per year, and is every calculations_year in use backfilled?
  3. Seeded cap values differ from the spec. MpdGoalMiscConstantsSeeder has 80k/125k/90k/145k; goal_calculator_spec.rb stubs 85k. Which figures are current?

Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/useMpdGoalPreview.ts Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsWarning.tsx Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/useMpdGoalPreview.ts Outdated
setPreview({
attributes: debounced,
monthlyGoal: null,
salaryOverCap: null,

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] **A failed preview silently erases a warning the user already raised — and nothing tests it.**

This .catch sets salaryOverCap: null, which line 152's ?? coalesces to savedSalaryOverCap. So an admin who raises the salary over cap, sees the warning, then makes any further edit whose preview request fails, watches the warning disappear — and can reasonably read that as "resolved". There is no snackbar (context: { suppressErrors: true }, line 108) and GoalSettingsWarning never consumes the hook's failed flag, so unlike MpdGoalPreview — which renders "Preview unavailable" for the same failure — the warning bar shows nothing at all.

This also contradicts the flicker fix's own principle: every other path holds the last known answer rather than reverting to saved.

Untested. Changing this null to false left 165 of 165 tests passing. It became unreachable from the suite when the mocks prop was removed from GoalSettingsWarning.test.tsx — that prop was the only handle for forcing a failure.

Two defensible resolutions:

// (a) hold the last known verdict rather than dropping it
.catch(() => {
  if (active) {
    setPreview((prev) => ({
      attributes: debounced,
      monthlyGoal: null,
      salaryOverCap: prev?.salaryOverCap ?? null,
    }));
  }
});

or (b) keep reverting to saved, but have GoalSettingsWarning render the failed state the way MpdGoalPreview does, so the disappearance is explained rather than silent.

Either way, add a test — re-introducing the mocks override (the pattern still present in MpdGoalPreview.test.tsx) is enough.

Severity is bounded because the warning is advisory, never gates Save, and self-corrects on the next successful preview or on save.

Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsForm.test.tsx Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsWarning.tsx Outdated
savedSalaryOverCap = false,
previewSalaryOverCap = false,
}) => (
<GqlMockedProvider<{

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] **No `ThemeProvider`, unlike every sibling.**

11 test files in this directory wrap in <ThemeProvider theme={theme}>, including the closest analogue MpdGoalPreview.test.tsx:56-58.

GoalSettingsWarning.tsx:35-36 styles on warning.main, so in tests it resolves against MUI's default palette rather than src/theme. The suite therefore cannot catch a token that exists in the default theme but not the project's — which is precisely the situation here (palette.warning is undefined in src/theme.ts, see the review body).

Low blast radius, but it's the reason the contrast issue was invisible to CI.

Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsWarning.test.tsx Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsForm.test.tsx Outdated
@canac

canac commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@zweatshirt Are you also going to add warnings that the user is healthcare exempt, SECA exempt, or over the debt cap?

(Sorry I missed the debt cap before with the API PR. I haven't dug in, but that check may need to happen on the server.)

@zweatshirt

Copy link
Copy Markdown
Contributor Author

@canac I can add that to this PR. My brain assumed we weren't adding them due to what we concluded with the backend PR, but it makes sense we want them still.

@canac

canac commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Yesterday, I was just thinking about the NSGC!DA5 checks, but it slipped my mind that we also need to implement NSGC!A33: IF(CV5<>"No","SECA Opted Out("&CV5&") - ","")&IF(CU5<>"No","Healthcare Exempt("&CU5&") - ","")&IF(CW5<>"No","Allow OverSalCap("&CW5&") - ","")&IF(CX5<>"No","Allow OverDebtCap("&CX5&") - ",""). Sorry about that!

@zweatshirt

Copy link
Copy Markdown
Contributor Author

@canac The backend PR added a boolean for healthcare exemption but you mentioned we don't need to check for it and so I removed it, are we still wanting to implement it?

@canac

canac commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

The sheet just surfaces a non-blocking warning when someone has an opt out or exemption. I think we can just read the booleans from the goal calculation and show a message if any of them are true. Like it says something like "SECA Opted Out (Both)", for example. I don't think that needs to be a validation in the backend calculations. What do you think?

@zweatshirt

zweatshirt commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@canac That was what I was intending with the boolean in the GraphQL for healthcare exemption. Is the goal to do as much of the logic for the warnings in the the frontend then? My idea was that we could send booleans to the frontend on which warnings to display and have the frontend act on those (or in the case of SECA we could maybe do an enum)

I guess the frontend has enough information for most of these apart from the salary cap and debt cap to do it in the frontend instead, though

@zweatshirt zweatshirt added the Staging API Run GraphQL codegen against the staging API label Jul 22, 2026
@zweatshirt

Copy link
Copy Markdown
Contributor Author

@canac This is basically ready for review. I've done multiple review agent iterations, it is considered critical. Sorry, it turned out to be a much bigger PR than I expected. Feel free to share constructive criticism.

@zweatshirt zweatshirt changed the title MPDX-9818 - Add salary over cap warning to goal settings and related tests MPDX-9818 - Add DA5 and A33 warnings/errors to goal settings Jul 22, 2026
@zweatshirt zweatshirt removed the Staging API Run GraphQL codegen against the staging API label Jul 22, 2026
@zweatshirt
zweatshirt requested review from canac and removed request for canac July 23, 2026 19:41
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.

2 participants