Repository navigation
Conversation
Co-authored-by: dmkt9 <dmkt9@users.noreply.github.com>
|
I created this PR from an upstream branch because I couldn't find a fork of App under To take ownership of this branch and push updates yourself, run: Then you can close this PR and open a new one from your fork. You can also keep this PR and ask me for changes with |
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
|
🤖 Web test steps verification results: The PR's Tests section has no manual steps yet, so I tested the expected behavior from the linked issue. The Reimbursable toggle works on cash expenses, and bulk edit shows the Reimbursable row when no selected expense is forced. I couldn't test company card or Expensify Card expenses: adding company cards needs an emailed validation code, and issuing an Expensify Card needs a bank account.
To test steps 3, 4, 5 and 6b, you need an account with company card feeds already set up (one Do Not Force, one Force Yes or Force No) and an Expensify Card. |
|
@dmkt9 can you get this in review today? |
Sure. Working on this |
|
@MelvinBot Resolving duplicates can silently change a company card expense from |
Resolving duplicates forced every kept managed card expense to non-reimbursable, which silently turned "Force Yes" company card expenses into non-reimbursable ones. Only Expensify Card expenses are now forced to false; company card expenses that can't be changed keep their own value, and "Do Not Force" ones show the review step. Co-authored-by: dmkt9 <dmkt9@users.noreply.github.com>
|
Fixed in ab4e096. Resolving duplicates no longer changes a forced company card expense's Reimbursable value. Cause: The duplicate review treated every managed card like an Expensify Card. It set Fix:
Notes and tests
|
|
@MelvinBot update your checklist:
|
|
I updated the PR description:
I kept the existing "Verify that no errors appear in the JS console" checkbox in |
|
@MelvinBot mark this PR as ready |
|
This PR is now marked as ready for review. |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ab4e09638f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| return ( | ||
| forceReimbursable === CONST.COMPANY_CARD.FORCE_REIMBURSABLE.DEFAULT_YES || | ||
| forceReimbursable === CONST.COMPANY_CARD.FORCE_REIMBURSABLE.DEFAULT_NO || | ||
| forceReimbursable === CONST.COMPANY_CARD.FORCE_REIMBURSABLE.NONE |
There was a problem hiding this comment.
Show the read-only toggle for Force Yes
When a company-card expense is stamped force_yes, this predicate returns false, and every new caller uses that result as a visibility condition. Consequently, the Reimbursable toggle is hidden entirely, although Force Yes is supposed to display the enabled value while preventing edits; visibility and editability need to be represented separately so only force_no hides the field.
Useful? React with 👍 / 👎.
| const shouldShowReimbursable = | ||
| (isPolicyExpenseChat || (isExpenseUnreported && !!policy)) && | ||
| (policy?.disabledFields?.reimbursable !== true || isCurrentTransactionReimbursableDifferentFromPolicyDefault) && | ||
| !isManagedCardTransactionTransactionUtils(transaction) && | ||
| (isManagedCardTransactionTransactionUtils(transaction) || policy?.disabledFields?.reimbursable !== true || isCurrentTransactionReimbursableDifferentFromPolicyDefault) && | ||
| isCardTransactionReimbursableEditable(transaction, transactionCardFeedSettings) && |
There was a problem hiding this comment.
Apply the feed rule to the confirmation footer
This updates the expense-detail visibility logic, but MoneyRequestConfirmationListFooter/hooks/useFooterDerivedFlags.ts still gates Reimbursable with !isManagedCardTransaction(transaction). Thus, a Do Not Force company-card expense continues to lose the toggle on the confirmation surface, leaving that part of the reported workflow unable to edit reimbursability.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 0f3b326. The confirmation footer now uses the same card feed rule as the expense view.
useFooterDerivedFlags.ts:112-118replaces!isManagedCardTransaction(transaction)withisCardTransactionReimbursableEditable(transaction, transactionCardFeedSettings). It looks up the feed settings the same wayMoneyRequestViewdoes. Card expenses also skip the workspace cash expense rule here.derivedFlagsSliceSelectornow includesbank,cardID, and onlycomment.forceReimbursable, so editing the description doesn't re-render this hook.isExpensifyCardTransactionandisCardTransactionReimbursableEditablenow accept a partial transaction, so the slice can be passed in.
Typecheck, the React Compiler check, and ESLint on the hook pass. The confirmation list, confirmation page, and TransactionUtils test suites also pass (568 tests). I didn't test it in the app because the test account has no company card feed.
| } else if (!isCardTransactionReimbursableEditable(selectedTransaction)) { | ||
| keep[fieldName] = getReimbursable(selectedTransaction); |
There was a problem hiding this comment.
Pass feed settings when reviewing legacy duplicates
For a company-card transaction without the new comment.forceReimbursable stamp, this call omits the feed settings, so the helper returns false even when the actual feed is default_yes, default_no, or none. If that transaction is kept while merging a duplicate with a different reimbursable value, the review step is skipped and users cannot choose the value, unlike the expense and bulk-edit paths that explicitly fall back to the feed configuration.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in ea5f229. When the kept company card expense has no stamp, the duplicate review now falls back to its feed setting.
compareDuplicateTransactionFieldstakes the kept expense's feed settings and passes them toisCardTransactionReimbursableEditable.- The new
useTransactionCardFeedSettingshook reads the feed settings the same wayMoneyRequestViewdoes: one card lookup and one feed record. - All callers pass these settings: the 3 places that start the review (
TransactionPreview,MoneyRequestHeaderPrimaryAction,DynamicReviewPage) and all 7DynamicReview*Pagescreens. The screens must pass them too so they compute the same step list.
Tests
- I added 2 unit tests. An unstamped expense on a
default_nofeed shows the step. An unstamped expense on aforce_yesfeed keeps its value. compareDuplicateTransactionFieldstests: 46 passed.TransactionPreviewUtilstests: 87 passed.- Typecheck and the React Compiler check pass.
- ESLint reports only errors on existing lines I didn't change, such as
HeaderWithBackButtonimports and old type assertions.
| const shouldShowBillable = (isPolicyExpenseChat || isExpenseUnreported) && (!!transactionBillable || isBillableEnabledOnPolicy(policy) || !!updatedTransaction?.billable); | ||
| const isCurrentTransactionReimbursableDifferentFromPolicyDefault = | ||
| policy?.defaultReimbursable !== undefined && !!(updatedTransaction?.reimbursable ?? transactionReimbursable) !== policy.defaultReimbursable; | ||
| const transactionCardFeedSettings = transactionCard ? cardFeedsForTransactionCard?.settings?.companyCards?.[getCompanyCardFeed(transactionCard.bank)] : undefined; |
There was a problem hiding this comment.
❌ CONSISTENCY-3 (docs)
This inline feed-settings lookup cardFeedsForTransactionCard?.settings?.companyCards?.[getCompanyCardFeed(transactionCard.bank)] duplicates the core logic of the existing getFeedSettingsForCard helper in CardFeedUtils (which this same PR exports and uses in SearchEditMultiplePage.tsx), and the identical expression is copied again in DynamicSplitExpenseEditPage.tsx. Three copies of the same settings.companyCards[getCompanyCardFeed(bank)] indexing will drift if the feed-settings shape ever changes.
Extract a small shared helper that resolves feed settings from a single feed record, e.g.:
// in CardFeedUtils.ts
function getFeedSettingsFromFeed(feed: OnyxEntry<CardFeeds[string]>, bank?: string): CustomCardFeedData | undefined {
return bank ? feed?.settings?.companyCards?.[getCompanyCardFeed(bank)] : undefined;
}Then call it from both edit pages:
const transactionCardFeedSettings = transactionCard ? getFeedSettingsFromFeed(cardFeedsForTransactionCard, transactionCard.bank) : undefined;Reviewed at: ab4e096 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
There was a problem hiding this comment.
Done in 57bf4d9. I added getFeedSettingsFromCardFeeds(cardFeeds, card) to src/libs/CardFeedUtils.ts:293. getFeedSettingsForCard now uses it too. This line now calls the helper, and so do DynamicSplitExpenseEditPage and the confirmation footer hook (useFooterDerivedFlags), which had a third copy.
| const shouldShowBillable = (isPolicyExpenseChat || isExpenseUnreported) && (!!splitExpenseDraftTransactionDetails?.billable || isBillableEnabledOnPolicy(effectivePolicy)); | ||
| const currentReimbursable = splitExpenseDraftTransaction?.reimbursable ?? transaction?.reimbursable; | ||
| const isReimbursableDifferentFromPolicyDefault = effectivePolicy?.defaultReimbursable !== undefined && !!currentReimbursable !== effectivePolicy.defaultReimbursable; | ||
| const transactionCardFeedSettings = transactionCard ? cardFeedsForTransactionCard?.settings?.companyCards?.[getCompanyCardFeed(transactionCard.bank)] : undefined; |
There was a problem hiding this comment.
❌ CONSISTENCY-3 (docs)
This inline feed-settings lookup cardFeedsForTransactionCard?.settings?.companyCards?.[getCompanyCardFeed(transactionCard.bank)] is identical to the one added in MoneyRequestView.tsx and duplicates the core indexing logic of the existing getFeedSettingsForCard helper (exported by this same PR). Having the same lookup in three places invites drift when the feed-settings shape changes.
Extract a shared helper that resolves feed settings from a single feed record and reuse it in both edit pages instead of inlining the settings.companyCards[getCompanyCardFeed(bank)] chain:
const transactionCardFeedSettings = transactionCard ? getFeedSettingsFromFeed(cardFeedsForTransactionCard, transactionCard.bank) : undefined;Reviewed at: ab4e096 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
There was a problem hiding this comment.
@MelvinBot handle this #104057 (comment) and this #104057 (comment)
There was a problem hiding this comment.
Done in 57bf4d9. This line now calls the shared getFeedSettingsFromCardFeeds helper in src/libs/CardFeedUtils.ts:293, as do MoneyRequestView and useFooterDerivedFlags.
MoneyRequestView, DynamicSplitExpenseEditPage and the confirmation footer each indexed settings.companyCards by the card's feed inline. They now use getFeedSettingsFromCardFeeds, which getFeedSettingsForCard also uses. Co-authored-by: dmkt9 <dmkt9@users.noreply.github.com>
…uplicates Company card expenses imported before the feed's reimbursable setting was stamped on them hid the Reimbursable review step even on "Do Not Force" feeds. The duplicate review now passes the kept expense's feed settings to compareDuplicateTransactionFields, matching the expense view and bulk edit. Co-authored-by: dmkt9 <dmkt9@users.noreply.github.com>
|
MelvinBot has made 5 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR. Next steps: A human should review the recent commits and resolve the conflicting CI errors manually. |
|
@codex review |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
Reviewer Checklist
Screenshots/Videoschrome.mp4 |
|
@joekaufmanexpensify does this look good from a product perspective? Is it what you expected? |
|
I would like to test this one. I will spin up an ad hoc build to do that. 👍 |
|
🚧 joekaufmanexpensify has triggered a test Expensify/App build. You can view the workflow run here. |
This comment has been minimized.
This comment has been minimized.
|
Toggle and its placement look good. However, I noticed that if you change the reimbursable setting on the feed in Expensify Classic, it is impacting transactions that have already been imported in new Expensify. That is different from the behavior outlined in the OP of the issue and my understanding of how it works in Expensify Classic today. Can we update that? Please let me know once it's updated, and I will retest! |
|
@dmkt9 can you address Joe's comment today, please? Thanks! |
|
@MelvinBot address this #104057 (comment) |
… reimbursable value can change Changing the feed's reimbursable setting should only affect expenses imported afterward, so the current feed setting is no longer used as a fallback for expenses without a stamped value. Co-authored-by: dmkt9 <dmkt9@users.noreply.github.com>
|
Addressed this feedback in be30f93. The toggle now uses only the Important The backend doesn't stamp What changed
|
Since these transactions don't have a Could you please retest this? |
|
Yep, i'll retest 👍 |
|
🚧 joekaufmanexpensify has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
I retested, and it's still not working as I expect. Now, if I import some expenses with force to non-reimbursable and then adjust the feed setting such that it does not force, I am correctly not seeing the toggle on expenses that are already imported. However, if I import new expenses when the feed setting is set to either of the "do not force" options, we're not showing the toggle on the expenses at all. With the current iteration of the ad hoc build, there isn't any scenario where I seem to be able to get the toggle to show up. I investigated with Codex, and it thinks what I found with testing is expected based on the changes in the PR right now. Its analysis is below. It thinks we need additional frontend/backend changes for this to work as expected. Could you review and let me know your thoughts?
|
|
@dmkt9 what do you think about the above? |
@joekaufmanexpensify I'm not sure. According to the OP, we want to use the I think we need to update the backend, don't we? WDYT, @blimpich? |
|
@MelvinBot merge the latest main branch |
Co-authored-by: dmkt9 <dmkt9@users.noreply.github.com> # Conflicts: # src/components/MoneyRequestConfirmationList/sections/selectors.ts # src/components/MoneyRequestConfirmationListFooter/hooks/useFooterDerivedFlags.ts # src/components/ReportActionItem/MoneyRequestView.tsx # src/pages/iou/DynamicSplitExpenseEditPage.tsx
|
I merged the latest
(isManagedCardTransaction(transaction) || canShowReimbursableToggleForPolicy(policy, reimbursable)) && isCardTransactionReimbursableEditable(transaction);
Typecheck passes. I couldn't run lint or Jest here because the sandbox's Node version is older than the repo requires. CI will run those checks. |
|
Okay, sounds good. Let's see what Ben thinks 👍 |
|
If we have to update the backend for this to work correctly, then let's do that. |












Explanation of Change
New Expensify hid the Reimbursable toggle on every company card expense. Company card expenses now show the toggle when their card feed's reimbursable setting is a "Do Not Force" option, matching Expensify Classic.
CONST.COMPANY_CARD.FORCE_REIMBURSABLEand aforceReimbursablefield on the transaction comment, which holds the feed setting stamped on the expense at import.isCardTransactionReimbursableEditable(transaction)toTransactionUtils:true).false.default_yes,default_noandnoneallow changes, whileforce_yesandforce_nodon't. The current feed setting is never read, so changing it only affects expenses imported afterward.force_no, so the toggle stays hidden.MoneyRequestView), split edit (DynamicSplitExpenseEditPage) and bulk edit (SearchEditMultiplePage). Bulk edit hides the row if any selected card expense is forced, including unreported ones.disabledFields.reimbursable), because that rule only covers cash expenses. This keeps the toggle visible on "Always reimbursable/non-reimbursable" workspaces, and it doesn't disappear after one change.Fixed Issues
$ #103341
PROPOSAL: #103341 (comment)
Tests
Same as Offline tests
Offline tests
Prerequisites
managedCard: true.comment.forceReimbursable, use Debug mode to mock the values described below.1. Verify Do Not Force
reimbursable = truecomment.forceReimbursable = "default_yes"comment.forceReimbursable = "default_no"andcomment.forceReimbursable = "none".2. Verify Force
reimbursable = truecomment.forceReimbursable = "force_yes"reimbursable = falseandcomment.forceReimbursable = "force_no".3. Verify Bulk Edit
comment.forceReimbursable = "default_yes"or"default_no".comment.forceReimbursable = "force_yes"or"force_no".4. Verify feed changes don't affect existing expenses
comment.forceReimbursablevalue.comment.forceReimbursable = "force_no"on another Company Card expense, then set the feed to a "Do Not Force" option.QA Steps
Same as Offline tests
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
AI Tests
Ran locally by MelvinBot:
npm run typecheck: passednpm run lint-changed: passednpm run fmt(oxfmt): no changes outside this PRnpm run react-compiler-compliance-check checkon the three changed components: passed.check-changedcouldn't resolve the base branch in this environment.npm run spell-changed: passedTransactionUtilsTest(14 new tests),SearchEditMultiplePageTest(3 new tests),MoneyRequestViewTest,DebugUtilsTestandCardFeedUtilsTestall passed (350 tests across the four suites, plus the new helper tests).npm testsuite and the Storybook smoke test. These run in CI.forceReimbursable.After removing the feed-setting fallback (be30f93):
npm run typecheck: passednpm run linton the changed files: passednpm run react-compiler-compliance-check checkon the four changed components and hooks: passedTransactionUtilsTestandSearchEditMultiplePageTestpassed (519 tests).spell-changed, which weren't available in this environment. These run in CI.