Skip to content

Fix: unify optimistic managerID with API param in submitReport#84628

Draft
MelvinBot wants to merge 2 commits intomainfrom
claude-unifySubmitReportManagerID
Draft

Fix: unify optimistic managerID with API param in submitReport#84628
MelvinBot wants to merge 2 commits intomainfrom
claude-unifySubmitReportManagerID

Conversation

@MelvinBot
Copy link
Contributor

Explanation of Change

submitReport() in IOU/index.ts used two different functions to compute the approver:

  • The optimistic managerID (written to local Onyx state) was derived from getApprovalChain(policy, expenseReport).at(0), which prepends category/tag rule approvers before the submitsToforwardsTo chain.
  • The API parameter managerAccountID was derived from getSubmitToAccountID(policy, expenseReport), which only checks the first rule approver then falls back to getManagerAccountID().

When a policy has rule-based approvers configured, these two functions can diverge — the client shows one approver optimistically while routing to another on the backend. This also caused isAwaitingFirstLevelApproval() (which compares getSubmitToAccountID() with report.managerID) to return incorrect results, affecting the "Add Expenses" button visibility in Classic.

This PR unifies both code paths to use getSubmitToAccountID(), so the optimistic value matches the API parameter. Both the optimistic update and API parameter now reference a single managerID variable.

Fixed Issues

$ #83073
PROPOSAL: #83073 (comment)

Tests

  1. Configure a workspace with advanced approval workflow: User A submits to Approver B, Approver B forwards to Approver C
  2. As User A, create and submit an expense report in New Expensify
  3. Verify the report is routed to Approver B (the configured first-level approver), not Approver C
  4. Open the same report in Expensify Classic and verify state is consistent
  • Verify that no errors appear in the JS console

Offline tests

  1. Go offline
  2. Submit an expense report
  3. Verify the optimistic managerID shows the correct approver
  4. Go back online and verify the report resolves correctly with the backend

QA Steps

  1. Set up a workspace with advanced approval mode where User A submits to Approver B, and Approver B forwards to Approver C
  2. As User A, submit an expense report in New Expensify
  3. Verify the report routes to Approver B
  4. Open the report in Classic and verify the state is consistent (same approver, correct status)
  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native

N/A - Logic-only change, no UI modifications

Android: mWeb Chrome

N/A - Logic-only change, no UI modifications

iOS: Native

N/A - Logic-only change, no UI modifications

iOS: mWeb Safari

N/A - Logic-only change, no UI modifications

MacOS: Chrome / Safari

N/A - Logic-only change, no UI modifications

@MelvinBot MelvinBot requested a review from a team March 9, 2026 18:49
@MelvinBot
Copy link
Contributor Author

I created this PR from an upstream branch since I don't have push access to your fork.

To take ownership of this branch and be able to push updates, run:

git remote add upstream https://github.com/Expensify/App.git
git fetch upstream claude-unifySubmitReportManagerID
git checkout -b claude-unifySubmitReportManagerID upstream/claude-unifySubmitReportManagerID
git push -u origin claude-unifySubmitReportManagerID

Then you can close this PR and open a new one from your fork.

@MelvinBot
Copy link
Contributor Author

The failing check is unrelated to this PR.

Analysis: The build (Debug, true) check failed during the Android Gradle build because Google's Maven repository (dl.google.com) returned HTTP 502 (Bad Gateway) when resolving Kotlin dependencies (kotlin-compiler-embeddable:2.1.20 and kotlin-compiler-runner:2.1.20). This is a transient infrastructure issue with Google's servers.

Evidence: This PR only modifies src/libs/actions/IOU/index.ts (a TypeScript file). The failure occurred in the Gradle settings-plugin Kotlin compilation step, which is part of the Android build infrastructure and has no relation to the PR's code changes. Re-running the failed job should resolve this.

@parasharrajat
Copy link
Member

Merge main @MelvinBot

@parasharrajat
Copy link
Member

@MelvinBot Give me comprehensive testing steps for different types of approval and submitter setups. Also, give me steps to set these different test case.

Co-authored-by: parasharrajat <parasharrajat@users.noreply.github.com>
@MelvinBot MelvinBot force-pushed the claude-unifySubmitReportManagerID branch from 8d9b6ff to 8b932f2 Compare March 19, 2026 14:27
@MelvinBot
Copy link
Contributor Author

Merged main into this branch. The branch previously had unrelated history (no common ancestor with main), so I recreated it from the latest main with the same changes applied. The branch now has proper git history.

@MelvinBot
Copy link
Contributor Author

Comprehensive Testing Steps for Approval & Submitter Setups

This PR unifies the optimistic managerID and the API managerAccountID parameter in submitReport() to both use getSubmitToAccountID(). Below are test cases for each approval configuration, with setup instructions.

Accounts needed: At minimum 4 test accounts (User A, Approver B, Approver C, Admin/Owner). More may be needed for the rule-based tests.


Test Case 1: Basic Approval (Submit & Approve)

Setup:

  1. Create a workspace (or use an existing one)
  2. Go to Workspace > Workflows
  3. Set approval mode to Submit & Approve
  4. The default approver should be the workspace admin/owner (Approver B)
  5. Add User A as a member

Test:

  1. Log in as User A
  2. Create an expense and submit the report
  3. Verify: Report routes to Approver B (the default approver)
  4. Verify: managerID on the report matches Approver B's accountID
  5. Open in Classic and confirm the state is consistent

Test Case 2: Advanced Approval — Simple Chain (submitsTo + forwardsTo)

Setup:

  1. Create a Control (Corporate) workspace
  2. Go to Workspace > Workflows and set to Advanced Approval
  3. Configure the employee list:
    • User AsubmitsTo: Approver B
    • Approver BforwardsTo: Approver C
  4. No approval rules configured (leave policy.rules.approvalRules empty)

Test:

  1. Log in as User A, create an expense, and submit the report
  2. Verify: Report routes to Approver B (not Approver C)
  3. Verify: The optimistic managerID shows Approver B immediately
  4. Log in as Approver B, approve the report
  5. Verify: Report now routes to Approver C (via forwardsTo)

Test Case 3: Advanced Approval — Over-Limit Forwarding

Setup:

  1. Use the same Control workspace from Test Case 2
  2. Configure Approver B's employee record:
    • approvalLimit: 500 (in the workspace currency)
    • overLimitForwardsTo: Approver D (a 4th test account)
    • forwardsTo: Approver C
  3. Ensure User A → submitsTo: Approver B

Test (under limit):

  1. Log in as User A, create an expense for $100, submit
  2. Verify: Routes to Approver B
  3. Approver B approves
  4. Verify: Routes to Approver C (normal forwardsTo, since $100 < $500 limit)

Test (over limit):

  1. Log in as User A, create an expense for $1000, submit
  2. Verify: Routes to Approver B first
  3. Approver B approves
  4. Verify: Routes to Approver D (overLimitForwardsTo, since $1000 > $500 limit)

Test Case 4: Category-Based Rule Approver

Setup:

  1. Use a Control workspace with Advanced Approval enabled
  2. Go to Workspace > Categories and create categories: Travel, Office Supplies
  3. Go to Workspace > Rules (or Approval Rules config) and add a rule:
    • Category = Travel → Approver: Approver D
  4. Configure User A → submitsTo: Approver B
  5. No tag-based rules

Test (category rule matches):

  1. Log in as User A
  2. Create an expense categorized as Travel, submit the report
  3. Verify: Report routes to Approver D (the category rule approver), NOT Approver B
  4. Verify: The optimistic managerID shows Approver D immediately
  5. Check in Classic that the state is consistent

Test (category rule does not match):

  1. Log in as User A
  2. Create an expense categorized as Office Supplies (no rule for this category), submit
  3. Verify: Report routes to Approver B (falls back to submitsTo since no rule matches)

Test Case 5: Tag-Based Rule Approver

Setup:

  1. Use a Control workspace with Advanced Approval enabled
  2. Go to Workspace > Tags and create tags: Project Alpha, Project Beta
  3. Add an approval rule:
    • Tag = Project Alpha → Approver: Approver D
  4. Configure User A → submitsTo: Approver B
  5. No category-based rules

Test:

  1. Log in as User A
  2. Create an expense tagged Project Alpha, submit the report
  3. Verify: Report routes to Approver D (the tag rule approver)
  4. Verify: The optimistic managerID shows Approver D immediately

Test Case 6: Category Rule Takes Priority Over Tag Rule

Setup:

  1. Use a Control workspace with Advanced Approval enabled
  2. Add approval rules:
    • Category = Travel → Approver: Approver C
    • Tag = Project Alpha → Approver: Approver D
  3. Configure User A → submitsTo: Approver B

Test:

  1. Log in as User A
  2. Create a single expense with category Travel AND tag Project Alpha, submit
  3. Verify: Report routes to Approver C (category rule takes priority over tag rule)
  4. Verify: The optimistic managerID shows Approver C

Test Case 7: Multiple Transactions with Different Category Approvers

Setup:

  1. Use a Control workspace with Advanced Approval enabled
  2. Add approval rules:
    • Category = Travel → Approver: Approver C
    • Category = Meals → Approver: Approver D
  3. Configure User A → submitsTo: Approver B

Test:

  1. Log in as User A
  2. Create two expenses on the same report:
    • Expense 1: category = Travel (Approver C)
    • Expense 2: category = Meals (Approver D)
  3. Submit the report
  4. Verify: Report routes to the approver matching the first transaction (by created date). getSubmitToAccountID uses getFirstRuleApprover which breaks on the first category match in sorted transaction order
  5. Verify: The optimistic managerID matches the API managerAccountID (both from getSubmitToAccountID)

Test Case 8: Rule Approver Is the Submitter (Self-Approval Scenario)

Setup:

  1. Use a Control workspace with Advanced Approval enabled
  2. Add an approval rule:
    • Category = Travel → Approver: User A (the submitter themselves!)
  3. Configure User A → submitsTo: Approver B

Test:

  1. Log in as User A
  2. Create an expense categorized as Travel, submit
  3. Verify: Report routes to Approver B (NOT User A), because getFirstRuleApprover skips self-approvers and falls back to getManagerAccountID
  4. Verify: The optimistic managerID shows Approver B

Test Case 9: Submit & Close (Optional Approval)

Setup:

  1. Create a workspace
  2. Go to Workspace > Workflows and set approval mode to Submit & Close (a.k.a. OPTIONAL)
  3. Add User A as a member

Test:

  1. Log in as User A, create an expense, submit
  2. Verify: The report closes immediately (no approval routing)
  3. Verify: getSubmitToAccountID returns the default approver but the report status reflects closed/reimbursed

Test Case 10: No submitsTo Configured (Fallback to Default Approver)

Setup:

  1. Use a Control workspace with Advanced Approval enabled
  2. Ensure User A's employee record has no submitsTo field set
  3. The workspace default approver is Approver B (policy.approver)
  4. No approval rules configured

Test:

  1. Log in as User A, create an expense, submit
  2. Verify: Report routes to Approver B (the default approver, since submitsTo is not set)

Test Case 11: Offline Submission (Optimistic Verification)

Applies to any of the above configurations. Pick Test Case 2 or 4 for best coverage.

Test:

  1. Configure one of the setups above (e.g., Test Case 4 — category rule approver)
  2. Go offline (disable network)
  3. Create an expense (with the appropriate category), submit
  4. Verify: The report immediately shows the correct approver (optimistic managerID from getSubmitToAccountID)
  5. Go back online
  6. Verify: The report resolves correctly — the managerID matches what the backend set

Test Case 12: isAwaitingFirstLevelApproval Consistency

This tests that the "Add Expenses" button visibility is correct.

Setup: Use Test Case 4 setup (category rule approver = Approver D, submitsTo = Approver B)

Test:

  1. Log in as User A, create a Travel expense, submit
  2. Report routes to Approver D
  3. Verify in Classic: The report shows as awaiting first-level approval (since report.managerID === getSubmitToAccountID() should now be true)
  4. Verify: The "Add Expenses" button behaves correctly based on this state

Quick Reference: Approver Resolution Priority

Priority Condition Approver Source
1 Category approval rule matches Category rule approver
2 Tag approval rule matches (no category match) Tag rule approver
3 Advanced mode with submitsTo set employee.submitsTo
4 Fallback policy.approver or policy.owner

Note: The rule approver is skipped if it equals the submitter (self-approval prevention in getFirstRuleApprover).

@MelvinBot
Copy link
Contributor Author

The failing check is unrelated to this PR.

Failing check: build (developmentDebug, false)

Analysis: The build fails with:

Could not read script '/home/runner/_work/App/App/node_modules/expo/scripts/autolinking.gradle' as it does not exist.

This is an Android build environment issue where the expo autolinking script is missing from node_modules. This PR only modifies src/libs/actions/IOU/index.ts (a TypeScript logic change) and has no impact on Android build configuration or dependencies.

Evidence: The other Android build (build (Debug, true)) passed successfully, and all other checks (ESLint, typecheck, tests, prettier, etc.) are green. This appears to be an intermittent build infrastructure issue. Re-running the failing job should resolve it.

@MelvinBot
Copy link
Contributor Author

The failing checks are unrelated to this PR's changes.

Analysis: This PR only modifies src/libs/actions/IOU/index.ts (3 additions, 5 deletions — a pure TypeScript logic change). The two failing builds are caused by infrastructure-level issues:

  1. build (developmentDebug, false) — Android build fails with: Could not read script '.../node_modules/expo/scripts/autolinking.gradle' as it does not exist.
  2. build (New Expensify Dev, DebugDevelopment, false) — iOS build fails with: error: ambiguous implicit access level for import of 'ExpoModulesCore' and 'Expo' in ios/AppDelegate.swift

Evidence: Both errors are in Expo build infrastructure, not in any file changed by this PR. The same build failures are also occurring on main branch (e.g., runs for recent merges like #85621 and #85612 are also failing). All code-quality checks (ESLint, TypeScript, tests, Prettier, spellcheck, storybook) pass.

@parasharrajat
Copy link
Member

Screenshots

🔲 iOS / native

🔲 iOS / Safari

🔲 MacOS / Chrome

🔲 Android / Chrome

🔲 Android / native

@parasharrajat
Copy link
Member

Tested a few flows. Seems fine. need to test more. I will be away today for a few hours. So will check back in 12 hours.

@parasharrajat
Copy link
Member

@MelvinBot merge main.

…ortManagerID

Co-authored-by: parasharrajat <parasharrajat@users.noreply.github.com>
@MelvinBot
Copy link
Contributor Author

Done — merged main into this branch and pushed. ✅

@MelvinBot
Copy link
Contributor Author

The failing check (build (New Expensify Dev, DebugDevelopment, false)) is unrelated to this PR.

Analysis: The iOS build fails with a Swift import access level error in ios/AppDelegate.swift:

error: ambiguous implicit access level for import of 'ExpoModulesCore'; it is imported as 'internal' elsewhere
error: ambiguous implicit access level for import of 'Expo'; it is imported as 'internal' elsewhere

Evidence: This PR only modifies src/libs/actions/IOU/index.ts — a TypeScript file with no relation to the iOS native build. The same iOS build failure also appears on the main branch (run 23425917000), confirming this is a pre-existing issue.

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.

2 participants