Repository navigation
[$175] Missing attendees violation is hidden when an approver views another member's expense #104596
Description
Problem
An approver or admin doesn't see the "Multiple attendees required for this category." violation on another member's expense. The expense creator does see it.
The backend leaves out the report owner when it counts attendees. The client check leaves out the current viewer instead. So when the approver views the expense, the client leaves out the approver, counts the creator as an extra attendee, and hides the violation.
Steps to reproduce
Preconditions: a Control workspace with attendee tracking on, and a category that requires attendees. Invite a member.
- As the member, create an expense in that category. Leave the member as the only attendee.
- As the member, go to Spend > Expenses. The expense shows the attendees violation.
- Log in as the admin or approver, or clear the cache if already logged in.
- Go to Spend > Expenses.
Expected: The member's expense shows the "Multiple attendees required for this category." violation.
Actual: The violation is hidden for the approver.
Reproducible on staging.
| Creator | Approver |
|---|---|
![]() |
![]() |
Root cause
getIsMissingAttendeesViolation takes the viewer's personal details and leaves that person out of the attendee count (AttendeeUtils.ts:71-72).
The violation builder in ViolationsUtils already leaves out the report owner, with the current user as the fallback when the report isn't available (ViolationsUtils.ts:981-995). The two checks disagree whenever the viewer isn't the owner.
These callers pass the current user:
TransactionListItem/index.tsx:213(Spend > Expenses row)ExpenseReportListItem.tsx:204(Spend > Reports row)MoneyRequestView.tsx:811(expense view)ViolationsUtils.ts:594(violation-fixed check)
The confirmation page (useConfirmationValidation.ts) can keep the current user, because there the current user is the creator.
Suggested fix
Have getIsMissingAttendeesViolation and syncMissingAttendeesViolation leave out the report owner's login, like the backend does. Use the current user as the fallback for unreported expenses. Update every caller listed above.
Context
Found during review of Fix false missingAttendees violation in Search rows. See the review analysis and the screenshots.
The same problem was reported earlier in Attendee - Multiple attendee violation only applicable for the owner of the WS, which was closed in January 2026.
Issue Owner
Current Issue Owner: @VickyStashUpwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~022113018264415730660
- Upwork Job ID: 2113018264415730660
- Last Price Increase: 2026-10-21
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsCRITICAL


Activity
nkdengineer commentedon Oct 21, 2026
@MelvinBot
Let's update the image in the issue's description to be correct.
MelvinBot commentedon Oct 21, 2026
Done. I updated the Creator / Approver table in the issue description with your screenshots:
merexpense shows "Multiple attendees required for this category."merexpense fromnkde243shows no violation.The rest of the description is unchanged.
view run
MelvinBot commentedon Oct 21, 2026
Proposal
What is the root cause of that problem?
The Spend > Expenses row rechecks the attendees violation on the client. The client check leaves out the viewer when it counts attendees, but the backend leaves out the report owner.
When an approver views the member's expense, the client leaves out the approver. It then counts the creator as a real attendee and decides there's no violation.
syncMissingAttendeesViolationthen removes the backend's violation from the row.Code references
AttendeeUtils.ts:71-78:getIsMissingAttendeesViolationleaves outuserPersonalDetails.login/email, which is always the current user.AttendeeUtils.ts:122-125:syncMissingAttendeesViolationdrops the backend violation when the client check returns false.TransactionListItem/index.tsx:211-220: the row passescurrentUserDetails, even though the owner'ssubmitterLoginis already loaded at line 136.ViolationsUtils.ts:983-994: the client violation builder already leaves out the owner (ownerLoginParam ?? getCurrentUserEmail()), which matches the backend.These callers have the same mismatch but only add violations, so they can miss a client-added violation but can't hide a backend one:
ExpenseReportListItem.tsx:204(owner login:submitterLogin)MoneyRequestView.tsx:810(owner login:iouReportOwnerLogin, line 232)The two creation-flow callers,
useConfirmationValidation.ts:276andViolationsUtils.ts:594(viauseFormErrorManagement), are correct as is, because there the current user is the creator.Fix false missingAttendees violation in Search rows only changed where attendees are read from. It didn't change who gets left out.
What changes do you think we should make in order to solve the problem?
In
AttendeeUtils.ts, changegetIsMissingAttendeesViolationandsyncMissingAttendeesViolationto take the owner's login instead of the current user's personal details. Leave out attendees whoseemailmatches that login, the same wayViolationsUtilsdoes. If no login is available, use the same fallback: assume one attendee is the owner (attendees.length - 1).Pass the report owner's login, falling back to the current user's login for unreported expenses:
TransactionListItem:submitterLogin(this fixes the reported bug)ExpenseReportListItem:submitterLoginMoneyRequestView:iouReportOwnerLoginIn
useConfirmationValidationandgetIsViolationFixed, pass the current user's login. Behavior there doesn't change.Add unit tests in
tests/unit/AttendeeUtilsTest.ts:synckeeps the backend violation.Then update the existing
getIsViolationFixedtests inViolationUtilsTest.tsfor the new argument.What alternative solutions did you explore? (Optional)
Keep the current parameter type and have callers pass
{...currentUserDetails, login: ownerLogin, email: ownerLogin}. This is a smaller diff, but it hides the intent, so changing the parameter is clearer.Related issues and open questions
submitterLoginis undefined. The row then falls back to the current user and the old behavior returns for that row.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/104596)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run
melvin-bot commentedon Oct 21, 2026
Current assignee @nkdengineer is eligible for the External assigner, not assigning anyone new.
[-]Missing attendees violation is hidden when an approver views another member's expense[/-][+][$175] Missing attendees violation is hidden when an approver views another member's expense[/+]melvin-bot commentedon Oct 21, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022113018264415730660
melvin-bot commentedon Oct 21, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
melvin-bot commentedon Oct 22, 2026
📣 @Abdallah-1551! 📣
Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
Please follow these steps:
Format:
Abdallah-1551 commentedon Oct 22, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On Spend > Expenses, an approver or admin viewing another member's expense doesn't see the "Multiple attendees required for this category." violation, though the creator does. The member is the only attendee and the category requires several, so the backend sends
missingAttendees. The client then hides it for everyone except the creator.What is the root cause of that problem?
The backend and the client count attendees differently.
ViolationsUtils.ts:983-995: leave out the report owner. The owner isownerLoginParam ?? getCurrentUserEmail().AttendeeUtils.getIsMissingAttendeesViolationandsyncMissingAttendeesViolation(AttendeeUtils.ts:71-78,108-125): leave out the current viewer, taken fromuserPersonalDetails.login/email.When an approver views the member's expense, the client leaves out the approver's login, so the member counts as an extra attendee.
getIsMissingAttendeesViolationreturnsfalse.syncMissingAttendeesViolationthen sees a backendMISSING_ATTENDEESviolation, no client-side violation, and filters the backend violation out. That is why the violation disappears instead of just not being added.The check only works when the viewer is the owner. Search rows, the expense view and the reports list use it for any viewer, so they break for approvers.
What changes do you think we should make in order to solve the problem?
1.
src/libs/AttendeeUtils.ts: make the check owner-based.Replace the
userPersonalDetails: CurrentUserPersonalDetailsparameter withownerLogin: string | undefinedin both functions. Leave out attendees whose email matches the owner. This mirrorsViolationsUtils.Keep the existing threshold logic. Only the "who is left out" part changes.
syncMissingAttendeesViolationtakesownerLoginand passes it through togetIsMissingAttendeesViolation. Its add/remove logic stays the same.2. Update the callers that are wrong for non-owner viewers.
Pass the report owner's login, with the current user's login as the fallback for unreported expenses, or when the owner isn't known.
TransactionListItem/index.tsx(~211-220): passsubmitterLogin ?? currentUserDetails.login.submitterLoginis already loaded around line 136. This fixes the reported bug.ExpenseReportListItem.tsx(~204): passsubmitterLogin ?? currentUserDetails.login.MoneyRequestView.tsx(~810): passiouReportOwnerLogin ?? currentUserPersonalDetails.login. This variable is already defined near line 232.3. Callers where the current user is the creator.
useConfirmationValidation.ts:276: passcurrentUserPersonalDetails.login ?? currentUserPersonalDetails.email. Behavior is unchanged.ViolationsUtils.getIsViolationFixed(~594): add an optionalownerLoginparameter. Default it togetCurrentUserEmail()and pass it togetIsMissingAttendeesViolation. Callers that know the report owner can pass it. The creation flow keeps the current behavior.4. Make the owner lookup robust in Search rows.
If
submitterLoginis undefined because the owner's personal details aren't loaded, resolve it fromreport.ownerAccountIDthroughpersonalDetails. Do this before falling back to the current user. Otherwise the old bug returns for that row.5. Tests.
In
tests/unit/AttendeeUtilsTest.ts:ownerLogin = member@…, attendees[member]:getIsMissingAttendeesViolationreturnstrue.syncMissingAttendeesViolationwith that input and an existing backendMISSING_ATTENDEESkeeps the violation.true, as before.[member, approver]andownerLogin = member:false, because one non-owner attendee exists.ownerLoginundefined: falls back to the current user's email.Update the existing
getIsViolationFixedtests inViolationUtilsTest.tsfor the new signature.What alternative solutions did you explore? (Optional)
userPersonalDetailssignature and have callers pass{...currentUserDetails, login: ownerLogin, email: ownerLogin}. This is a smaller diff, but it is misleading and error-prone. Changing the parameter to an owner login states the intent.syncMissingAttendeesViolationin Search rows and trust the backend. This would fix the bug, but it would reintroduce the false positives that PR Fix false missingAttendees violation in Search rows #104540 fixed, so I rejected it.attendees.length - 1when no owner is known. This is wrong when the viewer-owner is not in the attendee list. Falling back to the current user's email matchesViolationsUtilsand is more predictable.Testing
npm test -- AttendeeUtilsTest ViolationUtilsTestand confirm the new and updated cases pass.MoneyRequestView) as the approver. The violation should also show there.bonnhatnguyen commentedon Oct 22, 2026
Contributor details
Your Expensify account email: [email protected]
Upwork Profile Link: https://www.upwork.com/freelancers/~0139ed4098bdc50182
Proposal
Please re-state the problem that we are trying to solve in this issue.
An approver or admin viewing another member's expense does not see the "Multiple attendees required for this category." violation on Spend > Expenses, whereas the expense creator sees the violation correctly.
What is the root cause of that problem?
getIsMissingAttendeesViolationinsrc/libs/AttendeeUtils.ts:71-78filters out attendees matchinguserPersonalDetails.loginoruserPersonalDetails.email, which represents the current viewer:TransactionListItem/index.tsx:211-220, the row passescurrentUserDetailstosyncMissingAttendeesViolation.false.syncMissingAttendeesViolation(AttendeeUtils.ts:122-125) then removes the backend'sMISSING_ATTENDEESviolation from the transaction, causing the violation to disappear for the approver.What changes do you think we should make in order to solve the problem?
In
src/libs/AttendeeUtils.ts:Refactor
getIsMissingAttendeesViolationandsyncMissingAttendeesViolationto acceptownerLogin: string | undefinedinstead ofuserPersonalDetails.Filter out attendees whose
emailmatchesownerLogin. IfownerLoginis undefined, fall back togetCurrentUserEmail().In
src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx:Pass the expense submitter/owner login (
submitterLogin ?? currentUserDetails.login) tosyncMissingAttendeesViolation. IfsubmitterLoginis not yet available, resolve it viareport.ownerAccountIDfrompersonalDetails.In
src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx:Pass
submitterLogin ?? currentUserDetails.login.In
src/components/ReportActionItem/MoneyRequestView.tsx:Pass
iouReportOwnerLogin ?? currentUserPersonalDetails.login.In
src/hooks/useConfirmationValidation.ts:276:Pass
currentUserPersonalDetails.login ?? currentUserPersonalDetails.email(behavior unchanged since creator is current user).In
src/libs/Violations/ViolationsUtils.ts(getIsViolationFixed):Pass
ownerLogintogetIsMissingAttendeesViolation.What alternative solutions did you explore? (Optional)
syncMissingAttendeesViolationfor non-owners: Rejected because that would leave stale violations if attendees were updated while viewing. Excluded attendee calculation based on report owner ensures 100% parity with backend business logic.What automated tests can you add to prevent this from re-occurring?
In
tests/unit/AttendeeUtilsTest.ts:ownerLoginis[email protected], attendees is[{email: '[email protected]'}], and viewer is[email protected]. AssertgetIsMissingAttendeesViolationreturnstrue.ownerLoginis[email protected]and attendees contains both[email protected]and[email protected]. AssertgetIsMissingAttendeesViolationreturnsfalse.melvin-bot commentedon Oct 22, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
VickyStash commentedon Oct 22, 2026
UPD: I haven't had enough time for this one today, but me or my teammate will take a look at it tomorrow!
Yesterday, I was able to reproduce the same bug as in the attached screenshots, and at first glance, it wasn't exactly due to the problem described in the issue. For me it was cause for the workspace member app didn't return
policyCategories_inOpenApp, so due topolicyCategories_missing, the app couldn't correctly handleareAttendeesRequiredcheck.It means that there are potentially two bugs to be addressed, but it should be re-verified.
rinej commentedon Oct 23, 2026
Hello, I'm Tomek from Callstack and I'm going to take this one over from @VickyStash
I've reproduced it and I'm working on a solution now. I am also checking the policyCategories_ missing from OpenApp that you mentioned, to verify if we got second bug or the same one. Will try to cover it in one fix
rinej commentedon Oct 23, 2026
I investigated the issue and reproduced it on staging with an admin as approver account and a member account.
I confirmed it's what the issue describes.
I also checked the missing policyCategories_ idea. In my repro both users had the categories loaded, so that wasn't the cause here.
Created the fix, the app now leaves out the expense owner when it counts attendees, the same as the backend does, so the warning shows the same for everyone. Before/after videos and more details are in the PR, so please have a look.
Here is the PR - #104855