Skip to content

[MPDX-9992] Split the MPGA salary row by person - #2057

Open
frett wants to merge 2 commits into
mainfrom
MPDX-9992-mpga-salary-per-person
Open

frett wants to merge 2 commits into
mainfrom
MPDX-9992-mpga-salary-per-person

Conversation

@frett

@frett frett commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Summary

The Staff Expense report already gives each spouse their own salary rows, but the MPGA report still rolled a couple's payroll into a single "Salary" line. This makes the MPGA report match.

  • Request each transaction's personNumber in the MPGA transactions query
  • Load the household from HCM in the MPGA context provider the way the Staff Expense report does. The HCM and report requests leave together, and the report counts as loading until both land, so salary is never shown combined and then split
  • Split the combined Salary row into one row per person, reader first, labeled "Salary (Name)" with the same translation string the Staff Expense report uses. Unattributed payroll stays with the reader, and a person HCM does not list reads "Spouse". A single staff member's row is unchanged, id included.
  • Each person's monthly amount is summed from their own transactions, as the Staff Expense report does, so a rounding gap between the API's month total and its transactions never becomes a phantom row for a person the month did not pay
  • Every split row keeps the Salary category, so the pie chart, totals, CSV export, sorting, and breakdown modal work as before. Each row's breakdown lists only that person's transactions and is titled with their name, e.g. "Salary (Alex) Breakdown".
  • The MPGA page reads a personNumber query param and the supervisor drawer's MPGA tab passes it in the link, matching the Staff Expense tab
  • HouseholdMember moves to Reports/Shared/Helpers/household.ts so both reports import it from one place

Per-subcategory rows shown when Salary is unchecked in Report Settings stay unsplit, matching the Staff Expense report where unchecking itemizes.

Jira

MPDX-9992

Testing

  • New useFilteredFunds tests: couple split with reader first, per-row breakdowns, unattributed payroll, blank person numbers, a month total that disagrees with its transactions, no phantom row for a person the month did not pay, single member, unknown person labeled "Spouse", no household, non-salary categories staying whole. Watched five fail before the implementation.
  • New context tests: HCM requested for self and for a viewed staff member, skipped for a supervisor without a person number, report requested without waiting on HCM, split visible through the provider
  • New tests for the supervisor tab link and the page param
  • One existing ExportCsvButton test clicked a menu item before data arrived. The added HCM round-trip exposed that, so it now waits for the item to enable like its siblings.
  • Type check, lint, and all Reports, hooks, MpdSupervisorReport, and reports page suites pass (118 suites, 809 tests)

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Bundle sizes [mpdx-react]

Compared against df619f6

Route Size (gzipped) Diff
/accountLists/[accountListId]/reports/mpgaIncomeExpenses 431.39 KB +1.1 KB

@frett
frett force-pushed the MPDX-9992-mpga-salary-per-person branch 6 times, most recently from 142873a to 911af15 Compare September 18, 2026 16:07
Comment thread src/components/Reports/MPGAIncomeExpensesReport/Helper/filterFunds.ts Outdated
const incomeMonthly = new Array(monthCount).fill(0);
const expenseMonthly = new Array(monthCount).fill(0);
const reader = splitsPerPerson(category, household)
? household[0].personNumber

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.

reader = household[0] is whoever Hcm was queried for (the login in own view, the drawer row's personNumber in supervisor view), not the staff account's primary; unattributed payroll is credited to and the table led by a different person depending on who views.

Failure scenario: Alex (primary) and Jordan (spouse) share one staffAccountId; schema documents hcm as 'Fetch HCM data for a person and their spouse, defaulting to yourself'. Alex logs in: unattributed payroll -> 'Salary (Alex)', listed first. Jordan logs in (or a supervisor opens Jordan's drawer row): same data, unattributed payroll -> 'Salary (Jordan)', listed first. Same account, different per-person totals. Pre-existing shared assumption with filterTransactions.tsx [reader] = household, but this PR extends it; at minimum the HouseholdMember docstring should say 'the person requested', not 'the staff member reading the report'.

// Only a missing one falls back to your own account.
// Only a missing one falls back to your own account and HCM record.
const staffAccountId = getQueryParam(query, 'staffAccountId') || undefined;
const personNumber = getQueryParam(query, 'personNumber') || undefined;

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.

personNumber is read from the URL independently of staffAccountId; a supervisor URL carrying only staffAccountId (bookmark, hand-built link, any producer other than ViewReportLink) skips Hcm and silently renders one combined Salary row for a couple, and a personNumber of a different supervised employee is never cross-checked against the account.

Failure scenario: Supervisor bookmarked ?staffAccountId=1000000001 before this PR -> skip: isSupervisorView && !personNumber -> combined 'Salary' row while the staff member's own view shows two rows; nothing hints the split exists. With ?staffAccountId=AAA&personNumber=BBB where BBB is another supervised employee, Hcm returns BBB's household, so AAA's real people render as 'Salary (Spouse)' and unattributed payroll is credited to BBB. Same pattern already exists on the staffExpense page, so this is a shared design gap rather than a regression.

() =>
hcmData?.hcm.map(({ staffInfo }) => ({
personNumber: staffInfo.personNumber,
name: staffInfo.preferredName ?? staffInfo.lastName,

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.

preferredName ?? lastName only falls back on null/undefined; an empty-string preferredName produces name: '', which fails the person ? ... : categoryLabel truthiness check downstream and leaves that spouse's split Salary row unlabeled.

Failure scenario: HCM returns { preferredName: '', lastName: 'Smith', personNumber: '000000111' } for the reader (preferredName is a nullable String with no client-side normalization). filterFunds: household.find(...)?.name ?? unknownName keeps '' (not null, so no 'Spouse' fallback), description = person ? ... : categoryLabel yields plain 'Salary' beside 'Salary (Jordan)'; BreakdownModal likewise shows 'Salary Breakdown'. preferredName || lastName closes it (same latent edge in StaffExpenseReport.tsx:108).

// Person numbers tell the reader's payroll from their spouse's. HCM lists the reader first, then
// their spouse. Both requests leave together; `loading` below covers them both so salary is not
// rendered as one household total and then split.
const { data: hcmData, loading: hcmLoading } = useHcmQuery({

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.

Cleanup: the useHcmQuery call (with skip: isSupervisorView && !personNumber) and the household useMemo mapping to { personNumber, name: preferredName ?? lastName } are copied line-for-line from StaffExpenseReport.tsx:100-111, and loading = reportLoading || hcmLoading from :154; the PR created Reports/Shared/Helpers/household.ts for the type but left the behavior duplicated.

Failure scenario: Two reports now own identical 15-line blocks that must agree on the skip rule, name fallback, error handling and loading gate; fixing any of the findings above (suppressErrors, || fallback, reader selection) has to be done twice and will drift. Extract useHousehold({ personNumber, isSupervisorView }) returning { household, loading } next to HouseholdMember and call it from both reports.

// Naming a person is only worth the noise once the report holds someone besides the reader.
const namePeople = people.size > 1;
// A person number HCM does not list belongs to neither spouse, so the household cannot name it.
const unknownName = t('Spouse');

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.

Cleanup: the per-person rules are now written in three places -- the naming block (t('Spouse'), household.find(...)?.name ?? unknownName, t('{{bucket}} ({{person}})')) is near-verbatim from filterTransactions.tsx:214-229, splitsPerPerson's === Salary duplicates aggregationPolicy.ts's perPerson: true, and BreakdownModal.tsx:46 re-checks category === Salary even though person is only ever set on the Salary path.

Failure scenario: Renaming the 'Spouse' fallback, changing the label format, or flagging another category perPerson desyncs the two reports the PR says should match, and the modal silently drops the name for any future split category (the new test 'ignores a person on any category other than salary' pins an unreachable state). Move householdMemberLabel(bucketLabel, personNumber, household, t) and a single splitsPerPerson(category) into Reports/Shared/Helpers/household.ts; in BreakdownModal trust person ? ... : ... (or pass the row's description in BreakdownTarget) and drop the enum import and that test.

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.

The Salary check in BreakdownModal is deliberate hardening: the modal should only ever name a person for the one category that splits per person, even if a caller passes a person for something else. The shared-helper extraction across both reports is agreed as a follow-up rather than part of this PR.

t,
incomeData,
expenseData,
household,

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.

(Refers to the addRowPerSubcategory branch at line 50, just above this hunk.)

Only the checked-category branch (addCombinedSubcategoryRow) receives household; unchecking Salary in Report Settings routes to addRowPerSubcategory, which collapses both spouses' payroll into one household 'Salary - Regular Pay' row.

Failure scenario: Couple's report shows 'Salary (Alex)' / 'Salary (Jordan)'. User unchecks Salary to itemize -> rows become a single 'Salary - Regular Pay' with both people's monthly totals summed and no person; re-checking splits them again. MPGA's itemized mode is the only place a couple's payroll is summed into a shared row (Staff Expense's itemized mode is one row per transaction, so inherently per person).

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.

Intentional. Unchecking Salary in Report Settings itemizes it, the same way unchecking a category itemizes in the Staff Expense report, and itemized rows are never split by person there either. Only the combined Salary row splits.

@frett
frett force-pushed the MPDX-9992-mpga-salary-per-person branch from 911af15 to f3db576 Compare September 18, 2026 16:53
frett and others added 2 commits September 18, 2026 11:12
The Staff Expense report already gives each spouse their own salary
rows, but the MPGA report still rolled a couple's payroll into a single
"Salary" line. Ask the API for each transaction's person number, load
the household from HCM the way the Staff Expense report does, and split
the combined Salary row into one row per person, reader first, labeled
"Salary (Name)". Unattributed payroll stays with the reader and a person
HCM does not list reads "Spouse". A single staff member's row is
unchanged.

Each person's monthly amount is summed from their own transactions, as
the Staff Expense report does, so a rounding gap between the API's
month total and its transactions never becomes a phantom row for a
person the month did not pay. Every split row keeps the Salary
category, so the pie chart, totals, CSV export, and breakdown modal
work as before.

The supervisor drawer's MPGA link now carries the person number so a
supervisor sees the same split. The HCM and report requests leave
together, and the report counts as loading until both land, so salary
is never shown combined and then split.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With salary split one row per person, both rows opened a breakdown
titled plain "Salary Breakdown" with different totals, so the two could
not be told apart. Carry the person's name on the row and into the
breakdown target, and compose the modal title and footer from the
category plus person using the same string the row label uses. Rows
that are not split are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@frett
frett force-pushed the MPDX-9992-mpga-salary-per-person branch from f3db576 to 29ccaff Compare September 18, 2026 17:12
@frett frett self-assigned this Sep 18, 2026
@frett
frett marked this pull request as ready for review September 18, 2026 17:15
@frett
frett requested a review from wjames111 September 18, 2026 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant