From 363564f50d64908a24c3d81ab0157c502a6d4bbe Mon Sep 17 00:00:00 2001 From: truph01 Date: Sat, 29 Aug 2026 11:29:07 +0700 Subject: [PATCH 1/4] refactor: thread currentAccountID via createOptionFromReport --- .../Search/FilterComponents/InSelector.tsx | 3 +- .../Search/SearchRouter/SearchRouter.tsx | 2 + src/libs/OptionsListUtils/index.ts | 3 + tests/unit/OptionsListUtilsTest.tsx | 70 +++++++++++++++++++ tests/unit/ReportActionsUtilsTest.ts | 2 +- 5 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/components/Search/FilterComponents/InSelector.tsx b/src/components/Search/FilterComponents/InSelector.tsx index 7f458edfa5ab..bc50de629d34 100644 --- a/src/components/Search/FilterComponents/InSelector.tsx +++ b/src/components/Search/FilterComponents/InSelector.tsx @@ -92,6 +92,7 @@ function InSelector({value = [], selectionListTextInputStyle, selectionListStyle conciergeReportID, reportAttributesDerived, isTrackIntentUser, + currentUserAccountID, }), ), isSelected, @@ -102,7 +103,7 @@ function InSelector({value = [], selectionListTextInputStyle, selectionListStyle const alternateText = getAlternateText( report, {}, - {dateFnsLocale, isReportArchived, personalDetails, policy, reportAttributesDerived, policyTags: reportPolicyTags, conciergeReportID, isTrackIntentUser}, + {dateFnsLocale, isReportArchived, personalDetails, policy, reportAttributesDerived, policyTags: reportPolicyTags, conciergeReportID, isTrackIntentUser, currentUserAccountID}, ); return {...report, alternateText}; }; diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 7c3733131375..30726c7bc03a 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -229,6 +229,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla showPersonalDetails: isOneOnOneChat(contextualReport), }, isTrackIntentUser, + currentUserAccountID, }); reportForContextualSearch = option; } @@ -299,6 +300,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla reportAttributes, isTrackIntentUser, dateFnsLocale, + currentUserAccountID, ], ); diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 11e7f67d225a..9982da1b083a 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -1967,6 +1967,7 @@ type CreateOptionFromReportParams = { policyTags?: OnyxEntry; visibleReportActionsData?: VisibleReportActionsDerivedValue; isTrackIntentUser?: boolean; + currentUserAccountID: number; }; function createOptionFromReport({ @@ -1982,6 +1983,7 @@ function createOptionFromReport({ policyTags, visibleReportActionsData = {}, isTrackIntentUser, + currentUserAccountID, }: CreateOptionFromReportParams) { const accountIDs = getParticipantsAccountIDsForDisplay(report); @@ -2001,6 +2003,7 @@ function createOptionFromReport({ visibleReportActionsData, sortedActions, isTrackIntentUser, + currentUserAccountID, }), }; } diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 953fdeb44c98..5e7fbe274ac6 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -10842,6 +10842,7 @@ describe('OptionsListUtils', () => { // When the threaded conciergeReportID matches the report const conciergeOption = createOptionFromReport({ dateFnsLocale: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, report, personalDetails: PERSONAL_DETAILS, privateIsArchived: undefined, @@ -10854,6 +10855,7 @@ describe('OptionsListUtils', () => { // And an identical report with a non-matching conciergeReportID is not treated as Concierge const regularOption = createOptionFromReport({ dateFnsLocale: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, report, personalDetails: PERSONAL_DETAILS, privateIsArchived: undefined, @@ -10883,6 +10885,7 @@ describe('OptionsListUtils', () => { const result = createOptionFromReport({ dateFnsLocale: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, report, personalDetails: PERSONAL_DETAILS, privateIsArchived: undefined, @@ -10915,6 +10918,7 @@ describe('OptionsListUtils', () => { const result = createOptionFromReport({ dateFnsLocale: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, report, personalDetails: PERSONAL_DETAILS, privateIsArchived: true, @@ -10946,6 +10950,7 @@ describe('OptionsListUtils', () => { const result = createOptionFromReport({ dateFnsLocale: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, report, personalDetails: PERSONAL_DETAILS, privateIsArchived: undefined, @@ -10977,6 +10982,7 @@ describe('OptionsListUtils', () => { const result = createOptionFromReport({ dateFnsLocale: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, report, personalDetails: PERSONAL_DETAILS, privateIsArchived: undefined, @@ -11009,6 +11015,7 @@ describe('OptionsListUtils', () => { const config = {showPersonalDetails: true}; const result = createOptionFromReport({ dateFnsLocale: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, report, personalDetails: PERSONAL_DETAILS, privateIsArchived: undefined, @@ -11046,6 +11053,7 @@ describe('OptionsListUtils', () => { const roomOption = createOptionFromReport({ dateFnsLocale: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, report, personalDetails: PERSONAL_DETAILS, privateIsArchived: undefined, @@ -11055,6 +11063,7 @@ describe('OptionsListUtils', () => { }); const personalDetailsOption = createOptionFromReport({ dateFnsLocale: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, report, personalDetails: PERSONAL_DETAILS, privateIsArchived: undefined, @@ -11068,6 +11077,66 @@ describe('OptionsListUtils', () => { // With showPersonalDetails the option is named after the other participant (account 1 in PERSONAL_DETAILS). expect(personalDetailsOption.text).toBe('Mister Fantastic'); }); + + it('should use the passed currentUserAccountID for the reimbursed preview wording', async () => { + // Dedicated reportID: module-level report-action caches survive Onyx.clear(), so writing + // REPORT_ACTIONS for a shared reportID would poison later tests that reuse it. + const reimbursedReportID = 'create-option-reimbursed-1'; + const ownerAccountID = 42; + const submitterLogin = 'submitter@expensify.com'; + + // Given an expense report whose last action is a Fast_ACH reimbursement paid to account 42 + const report: Report = { + reportID: reimbursedReportID, + reportName: 'Expense Report', + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID, + lastMessageText: '', + lastActionType: CONST.REPORT.ACTIONS.TYPE.REIMBURSED, + lastVisibleActionCreated: '2024-01-01 10:00:00.000', + }; + const reimbursedAction = { + actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSED, + reportActionID: 'reimbursed-1', + created: '2024-01-01 10:00:00.000', + actorAccountID: 3, + originalMessage: { + paymentMethod: 'Fast_ACH', + creditBankAccountLast4: '1111', + expectedDate: '2025-03-15', + }, + message: [{type: 'COMMENT', html: 'reimbursed', text: 'reimbursed'}], + } as unknown as ReportAction; + + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reimbursedReportID}`, report); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reimbursedReportID}`, {[reimbursedAction.reportActionID]: reimbursedAction}); + await waitForBatchedUpdates(); + + const personalDetailsWithSubmitter = { + ...PERSONAL_DETAILS, + [ownerAccountID]: {accountID: ownerAccountID, login: submitterLogin, displayName: 'Submitter'}, + }; + const params = { + dateFnsLocale: undefined, + report, + personalDetails: personalDetailsWithSubmitter, + privateIsArchived: undefined, + policy: undefined, + sortedActions: {[reimbursedReportID]: [reimbursedAction]}, + conciergeReportID: undefined, + config: {showChatPreviewLine: true}, + }; + + // When the option is built for the report owner + const ownOption = createOptionFromReport({...params, currentUserAccountID: ownerAccountID}); + + // And for somebody else + const otherOption = createOptionFromReport({...params, currentUserAccountID: 999}); + + // Then the wording follows the passed currentUserAccountID, not the module-level session value + expect(ownOption.alternateText).toContain('your bank account ending in 1111'); + expect(otherOption.alternateText).toContain(`${submitterLogin}'s bank account ending in 1111`); + }); }); describe('createFilteredOptionList', () => { @@ -11539,6 +11608,7 @@ describe('OptionsListUtils', () => { const result = createOptionFromReport({ dateFnsLocale: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, report, personalDetails: PERSONAL_DETAILS, privateIsArchived: undefined, diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index c2d3286f94d9..f3707180b80f 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -5368,7 +5368,7 @@ describe('ReportActionsUtils', () => { creditedAmount: 8050, }); - const result = ReportActionsUtils.getReimbursedMessage(translateLocal, undefined, action, 2, undefined, undefined, convertToDisplayString); + const result = ReportActionsUtils.getReimbursedMessage(translateLocal, undefined, action, 2, undefined, undefined, convertToDisplayString, CONST.DEFAULT_NUMBER_ID); // Then we describe the payment without an amount rather than guessing a currency expect(result).toBe( From 3d4efce131acaf107159e51708b1a0e8b8202a99 Mon Sep 17 00:00:00 2001 From: truph01 Date: Sat, 29 Aug 2026 11:35:34 +0700 Subject: [PATCH 2/4] fix: add test --- tests/unit/OptionsListUtilsTest.tsx | 150 +++++++++++++++++++--------- 1 file changed, 102 insertions(+), 48 deletions(-) diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 5e7fbe274ac6..2e999a4eb38a 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -11078,64 +11078,118 @@ describe('OptionsListUtils', () => { expect(personalDetailsOption.text).toBe('Mister Fantastic'); }); - it('should use the passed currentUserAccountID for the reimbursed preview wording', async () => { - // Dedicated reportID: module-level report-action caches survive Onyx.clear(), so writing - // REPORT_ACTIONS for a shared reportID would poison later tests that reuse it. - const reimbursedReportID = 'create-option-reimbursed-1'; - const ownerAccountID = 42; - const submitterLogin = 'submitter@expensify.com'; + describe('reimbursed report preview', () => { + const SUBMITTER_LOGIN = 'submitter@expensify.com'; + + /** + * Builds an expense report whose last visible action is a REIMBURSED action, writes it to Onyx and + * returns the params for `createOptionFromReport`. Each case uses its own reportID because the + * module-level report-action caches survive `Onyx.clear()`, so a shared ID would leak across tests. + */ + const setUpReimbursedReport = async (reportID: string, ownerAccountID: number, originalMessage: Record) => { + const report: Report = { + reportID, + reportName: 'Expense Report', + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID, + // Empty so the preview falls through to getLastMessageTextForReport instead of reusing lastMessageText. + lastMessageText: '', + lastActionType: CONST.REPORT.ACTIONS.TYPE.REIMBURSED, + lastVisibleActionCreated: '2024-01-01 10:00:00.000', + }; + const reimbursedAction = { + actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSED, + reportActionID: `reimbursed-${reportID}`, + created: '2024-01-01 10:00:00.000', + actorAccountID: 3, + originalMessage, + message: [{type: 'COMMENT', html: 'reimbursed', text: 'reimbursed'}], + } as unknown as ReportAction; + + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {[reimbursedAction.reportActionID]: reimbursedAction}); + await waitForBatchedUpdates(); - // Given an expense report whose last action is a Fast_ACH reimbursement paid to account 42 - const report: Report = { - reportID: reimbursedReportID, - reportName: 'Expense Report', - type: CONST.REPORT.TYPE.EXPENSE, - ownerAccountID, - lastMessageText: '', - lastActionType: CONST.REPORT.ACTIONS.TYPE.REIMBURSED, - lastVisibleActionCreated: '2024-01-01 10:00:00.000', + return { + dateFnsLocale: undefined, + report, + personalDetails: {...PERSONAL_DETAILS, [ownerAccountID]: {accountID: ownerAccountID, login: SUBMITTER_LOGIN, displayName: 'Submitter'}}, + privateIsArchived: undefined, + policy: undefined, + sortedActions: {[reportID]: [reimbursedAction]}, + conciergeReportID: undefined, + config: {showChatPreviewLine: true}, + }; }; - const reimbursedAction = { - actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSED, - reportActionID: 'reimbursed-1', - created: '2024-01-01 10:00:00.000', - actorAccountID: 3, - originalMessage: { + + it('should address the payee directly only in the preview shown to them for a Fast ACH reimbursement', async () => { + // Given an expense report whose last action is a Fast_ACH reimbursement paid to account 42 + const ownerAccountID = 42; + const params = await setUpReimbursedReport('create-option-reimbursed-fast-ach', ownerAccountID, { paymentMethod: 'Fast_ACH', creditBankAccountLast4: '1111', expectedDate: '2025-03-15', - }, - message: [{type: 'COMMENT', html: 'reimbursed', text: 'reimbursed'}], - } as unknown as ReportAction; + }); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reimbursedReportID}`, report); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reimbursedReportID}`, {[reimbursedAction.reportActionID]: reimbursedAction}); - await waitForBatchedUpdates(); + // When the option is built for the report owner, and for somebody else + const ownOption = createOptionFromReport({...params, currentUserAccountID: ownerAccountID}); + const otherOption = createOptionFromReport({...params, currentUserAccountID: 999}); - const personalDetailsWithSubmitter = { - ...PERSONAL_DETAILS, - [ownerAccountID]: {accountID: ownerAccountID, login: submitterLogin, displayName: 'Submitter'}, - }; - const params = { - dateFnsLocale: undefined, - report, - personalDetails: personalDetailsWithSubmitter, - privateIsArchived: undefined, - policy: undefined, - sortedActions: {[reimbursedReportID]: [reimbursedAction]}, - conciergeReportID: undefined, - config: {showChatPreviewLine: true}, - }; + // Then the wording follows the passed currentUserAccountID + expect(ownOption.alternateText).toContain('your bank account ending in 1111'); + expect(otherOption.alternateText).toContain(`${SUBMITTER_LOGIN}'s bank account ending in 1111`); + }); - // When the option is built for the report owner - const ownOption = createOptionFromReport({...params, currentUserAccountID: ownerAccountID}); + it('should address the payee directly only in the preview shown to them for a StripeConnect reimbursement', async () => { + // Given an expense report reimbursed to account 42 through StripeConnect + const ownerAccountID = 42; + const params = await setUpReimbursedReport('create-option-reimbursed-stripe', ownerAccountID, { + paymentMethod: 'StripeConnect', + creditBankAccountLast4: '2222', + stripePaymentType: 'bank_transfer', + }); + + // When the option is built for the report owner, and for somebody else + const ownOption = createOptionFromReport({...params, currentUserAccountID: ownerAccountID}); + const otherOption = createOptionFromReport({...params, currentUserAccountID: 999}); - // And for somebody else - const otherOption = createOptionFromReport({...params, currentUserAccountID: 999}); + // Then the wording follows the passed currentUserAccountID + expect(ownOption.alternateText).toContain('your bank account ending in 2222'); + expect(otherOption.alternateText).toContain(`${SUBMITTER_LOGIN}'s bank account ending in 2222`); + }); - // Then the wording follows the passed currentUserAccountID, not the module-level session value - expect(ownOption.alternateText).toContain('your bank account ending in 1111'); - expect(otherOption.alternateText).toContain(`${submitterLogin}'s bank account ending in 1111`); + it('should build the preview for the account it is given rather than the signed-in session account', async () => { + // Given a report owned by the signed-in user (the session accountID written in beforeEach) + const params = await setUpReimbursedReport('create-option-reimbursed-session', CURRENT_USER_ACCOUNT_ID, { + paymentMethod: 'Fast_ACH', + creditBankAccountLast4: '3333', + expectedDate: '2025-03-15', + }); + + // When the option is built for a different account than the one in the session + const option = createOptionFromReport({...params, currentUserAccountID: 999}); + + // Then the passed account wins over the module-level session value, so the owner is named explicitly + expect(option.alternateText).toContain(`${SUBMITTER_LOGIN}'s bank account ending in 3333`); + expect(option.alternateText).not.toContain('your bank account'); + }); + + it('should build the same ACH preview for every viewer', async () => { + // Given an expense report reimbursed with plain ACH, whose message never names the payee + const params = await setUpReimbursedReport('create-option-reimbursed-ach', 42, { + paymentMethod: 'ACH', + debitBankAccountLast4: '9999', + creditBankAccountLast4: '5678', + }); + + // When the option is built for the report owner, and for somebody else + const ownOption = createOptionFromReport({...params, currentUserAccountID: 42}); + const otherOption = createOptionFromReport({...params, currentUserAccountID: 999}); + + // Then both previews are identical + expect(ownOption.alternateText).toBe(otherOption.alternateText); + expect(ownOption.alternateText).toContain('to the bank account ending in 5678'); + }); }); }); From bd17d749dce34d6b3b7456cfacf2206f9fce1df3 Mon Sep 17 00:00:00 2001 From: truph01 Date: Sat, 29 Aug 2026 11:36:26 +0700 Subject: [PATCH 3/4] fix: revert redundant change --- tests/unit/ReportActionsUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index f3707180b80f..c2d3286f94d9 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -5368,7 +5368,7 @@ describe('ReportActionsUtils', () => { creditedAmount: 8050, }); - const result = ReportActionsUtils.getReimbursedMessage(translateLocal, undefined, action, 2, undefined, undefined, convertToDisplayString, CONST.DEFAULT_NUMBER_ID); + const result = ReportActionsUtils.getReimbursedMessage(translateLocal, undefined, action, 2, undefined, undefined, convertToDisplayString); // Then we describe the payment without an amount rather than guessing a currency expect(result).toBe( From c012537287c85ec07d1e689915fb876a08a69080 Mon Sep 17 00:00:00 2001 From: truph01 Date: Sat, 29 Aug 2026 11:46:36 +0700 Subject: [PATCH 4/4] fix: lint --- tests/unit/OptionsListUtilsTest.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 2e999a4eb38a..a6ad7a0f57ef 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -93,6 +93,7 @@ import type {PersonalDetails, Policy, Report, ReportAction, ReportNameValuePairs import type {ReportAttributes} from '@src/types/onyx/DerivedValues'; import type {Participant} from '@src/types/onyx/IOU'; import type Login from '@src/types/onyx/Login'; +import type {OriginalMessageReimbursed} from '@src/types/onyx/OriginalMessage'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -11086,7 +11087,7 @@ describe('OptionsListUtils', () => { * returns the params for `createOptionFromReport`. Each case uses its own reportID because the * module-level report-action caches survive `Onyx.clear()`, so a shared ID would leak across tests. */ - const setUpReimbursedReport = async (reportID: string, ownerAccountID: number, originalMessage: Record) => { + const setUpReimbursedReport = async (reportID: string, ownerAccountID: number, originalMessage: OriginalMessageReimbursed) => { const report: Report = { reportID, reportName: 'Expense Report', @@ -11097,14 +11098,14 @@ describe('OptionsListUtils', () => { lastActionType: CONST.REPORT.ACTIONS.TYPE.REIMBURSED, lastVisibleActionCreated: '2024-01-01 10:00:00.000', }; - const reimbursedAction = { + const reimbursedAction: ReportAction = { actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSED, reportActionID: `reimbursed-${reportID}`, created: '2024-01-01 10:00:00.000', actorAccountID: 3, originalMessage, message: [{type: 'COMMENT', html: 'reimbursed', text: 'reimbursed'}], - } as unknown as ReportAction; + }; await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {[reimbursedAction.reportActionID]: reimbursedAction});