From 98ca2cc2c849d52815001632a6e3970877b63f08 Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Wed, 1 Apr 2026 21:21:17 +0430 Subject: [PATCH 01/14] fix(LHN): remove brackets from expense amount in preview --- src/libs/OptionsListUtils/index.ts | 32 +++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 47ecd6259245..09d675a95363 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -10,6 +10,7 @@ import FallbackAvatar from '@assets/images/avatars/fallback-avatar.svg'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import type {PrivateIsArchivedMap} from '@hooks/usePrivateIsArchivedMap'; import {getEnabledCategoriesCount} from '@libs/CategoryUtils'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; import filterArrayByMatch from '@libs/filterArrayByMatch'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {isReportMessageAttachment} from '@libs/isReportMessageAttachment'; @@ -109,6 +110,7 @@ import { isTaskAction, isThreadParentMessage, isUnapprovedAction, + shouldReportActionBeVisible, withDEWRoutedActionsArray, } from '@libs/ReportActionsUtils'; import {getReportName} from '@libs/ReportNameUtils'; @@ -566,6 +568,34 @@ function hasHiddenDisplayNames(accountIDs: number[]) { return getPersonalDetailsByIDs({accountIDs, currentUserAccountID: 0}).some((personalDetail) => !getDisplayNameOrDefault(personalDetail, undefined, false)); } +function getCanonicalMoneyRequestPreviewText( + translate: LocalizedTranslate, + report: OnyxEntry, + reportID: string, + lastReportAction: OnyxEntry, + isReportArchived: boolean, +): string { + const canUserPerformWrite = canUserPerformWriteAction(report, isReportArchived); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const latestMoneyRequestAction = deprecatedAllSortedReportActions[reportID]?.find( + (reportAction, key): reportAction is ReportAction => + shouldReportActionBeVisible(reportAction, key, canUserPerformWrite) && + reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && + isMoneyRequestAction(reportAction), + ); + const latestMoneyRequestOriginalMessage = latestMoneyRequestAction ? getOriginalMessage(latestMoneyRequestAction) : undefined; + const latestMoneyRequestAmount = latestMoneyRequestOriginalMessage?.amount; + const latestMoneyRequestCurrency = latestMoneyRequestOriginalMessage?.currency ?? report?.currency; + const latestMoneyRequestComment = Parser.htmlToText(latestMoneyRequestOriginalMessage?.comment ?? '').trim(); + + if (isExpenseReport(report) && typeof latestMoneyRequestAmount === 'number' && latestMoneyRequestCurrency) { + const formattedAmount = convertToDisplayString(Math.abs(latestMoneyRequestAmount), latestMoneyRequestCurrency); + return formatReportLastMessageText(translate('iou.lhnExpenseAmount', formattedAmount, latestMoneyRequestComment || undefined)); + } + + return formatReportLastMessageText(Parser.htmlToText(getReportPreviewMessage(report, latestMoneyRequestAction ?? lastReportAction, true, false, null, true))); +} + function getLastActorDisplayNameFromLastVisibleActions( report: OnyxEntry, lastActorDetails: Partial | null, @@ -953,7 +983,7 @@ function getLastMessageTextForReport({ if (scanningTransactions.length > 0) { lastMessageTextFromReport = translate('iou.receiptScanning', {count: scanningTransactions.length}); } else if (report?.transactionCount && report?.transactionCount > 0 && report?.currency) { - lastMessageTextFromReport = lastVisibleMessage?.lastMessageText; + lastMessageTextFromReport = getCanonicalMoneyRequestPreviewText(translate, report, reportID, lastReportAction, isReportArchived) || lastVisibleMessage?.lastMessageText; } else if (report?.transactionCount === 0) { lastMessageTextFromReport = translate('report.noActivityYet'); } From 6d672f42021b1df391b7625be24125b05c1d700a Mon Sep 17 00:00:00 2001 From: Maruf Sharifi Date: Wed, 1 Apr 2026 22:23:56 +0430 Subject: [PATCH 02/14] add missed translation --- src/languages/en.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 6a970ee6af65..c4db8dba727a 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1399,6 +1399,7 @@ const translations = { flip: 'Flip', sendInvoice: (amount: string) => `Send ${amount} invoice`, expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? ` for ${comment}` : ''}`, + lhnExpenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount} expense${comment ? ` for ${comment}` : ''}`, submitted: (memo?: string) => `submitted${memo ? `, saying ${memo}` : ''}`, automaticallySubmitted: `submitted via delay submissions`, queuedToSubmitViaDEW: 'queued to submit via custom approval workflow', From d9dc63232779e4a0fe2eb30093a4173f6fdbb8bb Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Fri, 3 Apr 2026 10:08:50 +0430 Subject: [PATCH 03/14] refactor(lhn): extract canonical money request preview helper --- src/languages/en.ts | 1 - src/libs/OptionsListUtils/index.ts | 44 ++++++++++++++++++------------ 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 336b8b5b0287..329e033d8982 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1402,7 +1402,6 @@ const translations = { flip: 'Flip', sendInvoice: (amount: string) => `Send ${amount} invoice`, expenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount}${comment ? ` for ${comment}` : ''}`, - lhnExpenseAmount: (formattedAmount: string, comment?: string) => `${formattedAmount} expense${comment ? ` for ${comment}` : ''}`, submitted: (memo?: string) => `submitted${memo ? `, saying ${memo}` : ''}`, markedAsDone: (memo?: string) => `marked as done${memo ? `, saying ${memo}` : ''}`, automaticallySubmitted: `submitted via delay submissions`, diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 84808f181ee3..e2f492f7f817 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -110,7 +110,6 @@ import { isTaskAction, isThreadParentMessage, isUnapprovedAction, - shouldReportActionBeVisible, withDEWRoutedActionsArray, } from '@libs/ReportActionsUtils'; import {getReportName} from '@libs/ReportNameUtils'; @@ -568,32 +567,40 @@ function hasHiddenDisplayNames(accountIDs: number[]) { return getPersonalDetailsByIDs({accountIDs, currentUserAccountID: 0}).some((personalDetail) => !getDisplayNameOrDefault(personalDetail, undefined, false)); } +function getLatestVisibleMoneyRequestAction( + reportID: string, + canUserPerformWrite: boolean | undefined, + sortedReportActions: ReportAction[] = [], + visibleReportActionsData?: VisibleReportActionsDerivedValue, +): OnyxEntry> { + return sortedReportActions.find( + (reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction) && + reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && + isReportActionVisible(reportAction, reportID, canUserPerformWrite, visibleReportActionsData), + ); +} + function getCanonicalMoneyRequestPreviewText( - translate: LocalizedTranslate, report: OnyxEntry, reportID: string, lastReportAction: OnyxEntry, isReportArchived: boolean, + visibleReportActionsData?: VisibleReportActionsDerivedValue, ): string { const canUserPerformWrite = canUserPerformWriteAction(report, isReportArchived); - // eslint-disable-next-line @typescript-eslint/no-deprecated - const latestMoneyRequestAction = deprecatedAllSortedReportActions[reportID]?.find( - (reportAction, key): reportAction is ReportAction => - shouldReportActionBeVisible(reportAction, key, canUserPerformWrite) && - reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && - isMoneyRequestAction(reportAction), - ); - const latestMoneyRequestOriginalMessage = latestMoneyRequestAction ? getOriginalMessage(latestMoneyRequestAction) : undefined; - const latestMoneyRequestAmount = latestMoneyRequestOriginalMessage?.amount; - const latestMoneyRequestCurrency = latestMoneyRequestOriginalMessage?.currency ?? report?.currency; - const latestMoneyRequestComment = Parser.htmlToText(latestMoneyRequestOriginalMessage?.comment ?? '').trim(); + const latestMoneyRequestAction = getLatestVisibleMoneyRequestAction(reportID, canUserPerformWrite, deprecatedAllSortedReportActions[reportID], visibleReportActionsData); + const originalMessage = latestMoneyRequestAction ? getOriginalMessage(latestMoneyRequestAction) : undefined; + const amount = originalMessage?.amount; + const currency = originalMessage?.currency ?? report?.currency; + const comment = Parser.htmlToText(originalMessage?.comment ?? '').trim(); - if (isExpenseReport(report) && typeof latestMoneyRequestAmount === 'number' && latestMoneyRequestCurrency) { - const formattedAmount = convertToDisplayString(Math.abs(latestMoneyRequestAmount), latestMoneyRequestCurrency); - return formatReportLastMessageText(translate('iou.lhnExpenseAmount', formattedAmount, latestMoneyRequestComment || undefined)); + if (isExpenseReport(report) && typeof amount === 'number' && currency) { + const formattedAmount = convertToDisplayString(Math.abs(amount), currency); + return formatReportLastMessageText(`${formattedAmount} expense${comment ? ` for ${comment}` : ''}`); } - return formatReportLastMessageText(Parser.htmlToText(getReportPreviewMessage(report, latestMoneyRequestAction ?? lastReportAction, true, false, null, true))); + return formatReportLastMessageText(Parser.htmlToText(getReportPreviewMessage(report, undefined, latestMoneyRequestAction ?? lastReportAction, true, false, null, true))); } function getLastActorDisplayNameFromLastVisibleActions( @@ -984,7 +991,8 @@ function getLastMessageTextForReport({ if (scanningTransactions.length > 0) { lastMessageTextFromReport = translate('iou.receiptScanning', {count: scanningTransactions.length}); } else if (report?.transactionCount && report?.transactionCount > 0 && report?.currency) { - lastMessageTextFromReport = getCanonicalMoneyRequestPreviewText(translate, report, reportID, lastReportAction, isReportArchived) || lastVisibleMessage?.lastMessageText; + lastMessageTextFromReport = + getCanonicalMoneyRequestPreviewText(report, reportID, lastReportAction, isReportArchived, visibleReportActionsDataParam) || lastVisibleMessage?.lastMessageText; } else if (report?.transactionCount === 0) { lastMessageTextFromReport = translate('report.noActivityYet'); } From dc64f8ba9c202f34513d2dd2ee3bbe4cdcfae8c5 Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Fri, 3 Apr 2026 10:18:05 +0430 Subject: [PATCH 04/14] add unit tests --- tests/unit/OptionsListUtilsTest.tsx | 112 ++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 68ccca660fcd..1e2da46da96a 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -4058,6 +4058,118 @@ describe('OptionsListUtils', () => { expect(formattedMessage).toBe('$1.00 for A A A'); }); }); + describe('canonical money request preview fallback', () => { + it('should format expense preview without brackets when falling back for expense reports', async () => { + const report: Report = { + ...createRandomReport(0, undefined), + reportID: 'expense-report-1', + type: CONST.REPORT.TYPE.EXPENSE, + currency: CONST.CURRENCY.USD, + transactionCount: 1, + }; + const createdAction: ReportAction = { + ...createRandomReportAction(1), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + message: [{type: 'COMMENT', text: ''}], + originalMessage: {}, + }; + const moneyRequestAction: ReportAction = { + ...createRandomReportAction(2), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: '2026-04-01 10:00:00.000', + message: [{type: 'COMMENT', text: ''}], + originalMessage: { + amount: -2500, + currency: CONST.CURRENCY.USD, + comment: 'Dinner', + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [createdAction.reportActionID]: createdAction, + [moneyRequestAction.reportActionID]: moneyRequestAction, + }); + await waitForBatchedUpdates(); + + const lastMessage = getLastMessageTextForReport({ + translate: translateLocal, + report, + lastActorDetails: null, + isReportArchived: false, + lastAction: createdAction, + currentUserLogin: CURRENT_USER_EMAIL, + }); + + expect(lastMessage).toBe('$25.00 expense for Dinner'); + }); + + it('should ignore deleted money request actions when building canonical expense preview', async () => { + const report: Report = { + ...createRandomReport(0, undefined), + reportID: 'expense-report-2', + type: CONST.REPORT.TYPE.EXPENSE, + currency: CONST.CURRENCY.USD, + transactionCount: 1, + }; + const createdAction: ReportAction = { + ...createRandomReportAction(3), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + message: [{type: 'COMMENT', text: ''}], + originalMessage: {}, + }; + const deletedMoneyRequestAction: ReportAction = { + ...createRandomReportAction(4), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: '2026-04-01 11:00:00.000', + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + message: [{type: 'COMMENT', text: ''}], + originalMessage: { + amount: -9900, + currency: CONST.CURRENCY.USD, + comment: 'Deleted comment', + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + const visibleMoneyRequestAction: ReportAction = { + ...createRandomReportAction(5), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: '2026-04-01 10:00:00.000', + message: [{type: 'COMMENT', text: ''}], + originalMessage: { + amount: -4500, + currency: CONST.CURRENCY.USD, + comment: 'Visible comment', + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [createdAction.reportActionID]: createdAction, + [deletedMoneyRequestAction.reportActionID]: deletedMoneyRequestAction, + [visibleMoneyRequestAction.reportActionID]: visibleMoneyRequestAction, + }); + await waitForBatchedUpdates(); + + const lastMessage = getLastMessageTextForReport({ + translate: translateLocal, + report, + lastActorDetails: null, + isReportArchived: false, + lastAction: createdAction, + currentUserLogin: CURRENT_USER_EMAIL, + }); + + expect(lastMessage).toBe('$45.00 expense for Visible comment'); + }); + }); it('MOVED_TRANSACTION action', async () => { const mockIsSearchTopmostFullScreenRoute = jest.mocked(isSearchTopmostFullScreenRoute); mockIsSearchTopmostFullScreenRoute.mockReturnValue(false); From f4af4e2ac99259de1235a68fd679e59157fbc88f Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Fri, 3 Apr 2026 10:27:03 +0430 Subject: [PATCH 05/14] add comment for deprecated sort actions --- src/libs/OptionsListUtils/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index e2f492f7f817..24b7fa66db4c 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -589,6 +589,7 @@ function getCanonicalMoneyRequestPreviewText( visibleReportActionsData?: VisibleReportActionsDerivedValue, ): string { const canUserPerformWrite = canUserPerformWriteAction(report, isReportArchived); + // eslint-disable-next-line @typescript-eslint/no-deprecated const latestMoneyRequestAction = getLatestVisibleMoneyRequestAction(reportID, canUserPerformWrite, deprecatedAllSortedReportActions[reportID], visibleReportActionsData); const originalMessage = latestMoneyRequestAction ? getOriginalMessage(latestMoneyRequestAction) : undefined; const amount = originalMessage?.amount; From 48ca09ede3debb3fc27fbbe8f4373e3fa85d03bd Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Thu, 9 Apr 2026 10:00:06 +0430 Subject: [PATCH 06/14] address feedbacks --- src/libs/OptionsListUtils/index.ts | 44 ++----- tests/unit/OptionsListUtilsTest.tsx | 198 +++++++++++++++++++++++++++- 2 files changed, 207 insertions(+), 35 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 40e4761ec38f..916e43d060e1 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -10,7 +10,6 @@ import FallbackAvatar from '@assets/images/avatars/fallback-avatar.svg'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import type {PrivateIsArchivedMap} from '@hooks/usePrivateIsArchivedMap'; import {getEnabledCategoriesCount} from '@libs/CategoryUtils'; -import {convertToDisplayString} from '@libs/CurrencyUtils'; import filterArrayByMatch from '@libs/filterArrayByMatch'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {isReportMessageAttachment} from '@libs/isReportMessageAttachment'; @@ -580,29 +579,6 @@ function getLatestVisibleMoneyRequestAction( ); } -function getCanonicalMoneyRequestPreviewText( - report: OnyxEntry, - reportID: string, - lastReportAction: OnyxEntry, - isReportArchived: boolean, - visibleReportActionsData?: VisibleReportActionsDerivedValue, -): string { - const canUserPerformWrite = canUserPerformWriteAction(report, isReportArchived); - // eslint-disable-next-line @typescript-eslint/no-deprecated - const latestMoneyRequestAction = getLatestVisibleMoneyRequestAction(reportID, canUserPerformWrite, deprecatedAllSortedReportActions[reportID], visibleReportActionsData); - const originalMessage = latestMoneyRequestAction ? getOriginalMessage(latestMoneyRequestAction) : undefined; - const amount = originalMessage?.amount; - const currency = originalMessage?.currency ?? report?.currency; - const comment = Parser.htmlToText(originalMessage?.comment ?? '').trim(); - - if (isExpenseReport(report) && typeof amount === 'number' && currency) { - const formattedAmount = convertToDisplayString(Math.abs(amount), currency); - return formatReportLastMessageText(`${formattedAmount} expense${comment ? ` for ${comment}` : ''}`); - } - - return formatReportLastMessageText(Parser.htmlToText(getReportPreviewMessage(report, undefined, latestMoneyRequestAction ?? lastReportAction, true, false, null, true))); -} - function getLastActorDisplayNameFromLastVisibleActions( report: OnyxEntry, lastActorDetails: Partial | null, @@ -745,14 +721,7 @@ function getLastMessageTextForReport({ const iouReportActions = iouReportID ? deprecatedAllSortedReportActions[iouReportID] : undefined; const canPerformWrite = canUserPerformWriteAction(report, isReportArchived); const lastIOUMoneyReportAction = - iouReportID && iouReportActions - ? iouReportActions.find( - (reportAction): reportAction is ReportAction => - isReportActionVisible(reportAction, iouReportID, canPerformWrite, visibleReportActionsForIOUReport) && - reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && - isMoneyRequestAction(reportAction), - ) - : undefined; + iouReportID && iouReportActions ? getLatestVisibleMoneyRequestAction(iouReportID, canPerformWrite, iouReportActions, visibleReportActionsForIOUReport) : undefined; // For workspace chats, use the report title if (reportUtilsIsPolicyExpenseChat(report) && !isEmptyObject(iouReport)) { @@ -990,8 +959,17 @@ function getLastMessageTextForReport({ if (scanningTransactions.length > 0) { lastMessageTextFromReport = translate('iou.receiptScanning', {count: scanningTransactions.length}); } else if (report?.transactionCount && report?.transactionCount > 0 && report?.currency) { + // eslint-disable-next-line @typescript-eslint/no-deprecated + const latestVisibleMoneyRequestAction = getLatestVisibleMoneyRequestAction( + reportID, + canUserPerformWrite, + deprecatedAllSortedReportActions[reportID], + visibleReportActionsDataParam, + ); lastMessageTextFromReport = - getCanonicalMoneyRequestPreviewText(report, reportID, lastReportAction, isReportArchived, visibleReportActionsDataParam) || lastVisibleMessage?.lastMessageText; + formatReportLastMessageText( + Parser.htmlToText(getReportPreviewMessage(report, conciergeReportID, latestVisibleMoneyRequestAction ?? lastReportAction, true, false, null, true)), + ) || lastVisibleMessage?.lastMessageText; } else if (report?.transactionCount === 0) { lastMessageTextFromReport = translate('report.noActivityYet'); } diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 5d78ec4ef9ac..8e60e421cd54 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -4059,6 +4059,9 @@ describe('OptionsListUtils', () => { }); }); describe('canonical money request preview fallback', () => { + const getExpectedPreviewText = (report: Report, reportAction: ReportAction, reportPreviewAction?: ReportAction) => + formatReportLastMessageText(Parser.htmlToText(getReportPreviewMessage(report, undefined, reportAction, true, false, null, true, reportPreviewAction))); + it('should format expense preview without brackets when falling back for expense reports', async () => { const report: Report = { ...createRandomReport(0, undefined), @@ -4104,7 +4107,7 @@ describe('OptionsListUtils', () => { currentUserLogin: CURRENT_USER_EMAIL, }); - expect(lastMessage).toBe('$25.00 expense for Dinner'); + expect(lastMessage).toBe(getExpectedPreviewText(report, moneyRequestAction)); }); it('should ignore deleted money request actions when building canonical expense preview', async () => { @@ -4167,7 +4170,198 @@ describe('OptionsListUtils', () => { currentUserLogin: CURRENT_USER_EMAIL, }); - expect(lastMessage).toBe('$45.00 expense for Visible comment'); + expect(lastMessage).toBe(getExpectedPreviewText(report, visibleMoneyRequestAction)); + }); + + it('should format amount-only preview when the canonical money request has an empty comment', async () => { + const report: Report = { + ...createRandomReport(0, undefined), + reportID: 'expense-report-3', + type: CONST.REPORT.TYPE.EXPENSE, + currency: CONST.CURRENCY.USD, + transactionCount: 1, + }; + const createdAction: ReportAction = { + ...createRandomReportAction(6), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + message: [{type: 'COMMENT', text: ''}], + originalMessage: {}, + }; + const moneyRequestAction: ReportAction = { + ...createRandomReportAction(7), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: '2026-04-01 12:00:00.000', + message: [{type: 'COMMENT', text: ''}], + originalMessage: { + amount: -2500, + currency: CONST.CURRENCY.USD, + comment: '', + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [createdAction.reportActionID]: createdAction, + [moneyRequestAction.reportActionID]: moneyRequestAction, + }); + await waitForBatchedUpdates(); + + const lastMessage = getLastMessageTextForReport({ + translate: translateLocal, + report, + lastActorDetails: null, + isReportArchived: false, + lastAction: createdAction, + currentUserLogin: CURRENT_USER_EMAIL, + }); + + expect(lastMessage).toBe(getExpectedPreviewText(report, moneyRequestAction)); + expect(lastMessage.endsWith('$25.00')).toBe(true); + }); + + it('should format preview correctly for non-USD currencies', async () => { + const report: Report = { + ...createRandomReport(0, undefined), + reportID: 'expense-report-4', + type: CONST.REPORT.TYPE.EXPENSE, + currency: CONST.CURRENCY.EUR, + transactionCount: 1, + }; + const createdAction: ReportAction = { + ...createRandomReportAction(8), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + message: [{type: 'COMMENT', text: ''}], + originalMessage: {}, + }; + const moneyRequestAction: ReportAction = { + ...createRandomReportAction(9), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: '2026-04-01 13:00:00.000', + message: [{type: 'COMMENT', text: ''}], + originalMessage: { + amount: -2500, + currency: CONST.CURRENCY.EUR, + comment: 'Lunch', + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [createdAction.reportActionID]: createdAction, + [moneyRequestAction.reportActionID]: moneyRequestAction, + }); + await waitForBatchedUpdates(); + + const lastMessage = getLastMessageTextForReport({ + translate: translateLocal, + report, + lastActorDetails: null, + isReportArchived: false, + lastAction: createdAction, + currentUserLogin: CURRENT_USER_EMAIL, + }); + + expect(lastMessage).toBe(getExpectedPreviewText(report, moneyRequestAction)); + }); + + it('should fall back to shared preview formatting when the canonical money request is missing amount', async () => { + const report: Report = { + ...createRandomReport(0, undefined), + reportID: 'expense-report-5', + type: CONST.REPORT.TYPE.EXPENSE, + currency: CONST.CURRENCY.USD, + transactionCount: 1, + }; + const createdAction: ReportAction = { + ...createRandomReportAction(10), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + message: [{type: 'COMMENT', text: ''}], + originalMessage: {}, + }; + const moneyRequestAction: ReportAction = { + ...createRandomReportAction(11), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: '2026-04-01 14:00:00.000', + message: [{type: 'COMMENT', text: ''}], + originalMessage: { + currency: CONST.CURRENCY.USD, + comment: 'Missing amount', + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [createdAction.reportActionID]: createdAction, + [moneyRequestAction.reportActionID]: moneyRequestAction, + }); + await waitForBatchedUpdates(); + + const lastMessage = getLastMessageTextForReport({ + translate: translateLocal, + report, + lastActorDetails: null, + isReportArchived: false, + lastAction: createdAction, + currentUserLogin: CURRENT_USER_EMAIL, + }); + + expect(lastMessage).toBe(getExpectedPreviewText(report, moneyRequestAction)); + }); + + it('should fall back to shared preview formatting when the canonical money request is missing currency', async () => { + const report: Report = { + ...createRandomReport(0, undefined), + reportID: 'expense-report-6', + type: CONST.REPORT.TYPE.EXPENSE, + currency: CONST.CURRENCY.USD, + transactionCount: 1, + }; + const createdAction: ReportAction = { + ...createRandomReportAction(12), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + message: [{type: 'COMMENT', text: ''}], + originalMessage: {}, + }; + const moneyRequestAction: ReportAction = { + ...createRandomReportAction(13), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: '2026-04-01 15:00:00.000', + message: [{type: 'COMMENT', text: ''}], + originalMessage: { + amount: -2500, + comment: 'Missing currency', + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [createdAction.reportActionID]: createdAction, + [moneyRequestAction.reportActionID]: moneyRequestAction, + }); + await waitForBatchedUpdates(); + + const lastMessage = getLastMessageTextForReport({ + translate: translateLocal, + report, + lastActorDetails: null, + isReportArchived: false, + lastAction: createdAction, + currentUserLogin: CURRENT_USER_EMAIL, + }); + + expect(lastMessage).toBe(getExpectedPreviewText(report, moneyRequestAction)); }); }); it('MOVED_TRANSACTION action', async () => { From 8fbb12ca369269f5e163da5251352034fdb54ae5 Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Thu, 9 Apr 2026 10:16:18 +0430 Subject: [PATCH 07/14] fix eslint --- src/libs/OptionsListUtils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 916e43d060e1..4c1370107b95 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -959,10 +959,10 @@ function getLastMessageTextForReport({ if (scanningTransactions.length > 0) { lastMessageTextFromReport = translate('iou.receiptScanning', {count: scanningTransactions.length}); } else if (report?.transactionCount && report?.transactionCount > 0 && report?.currency) { - // eslint-disable-next-line @typescript-eslint/no-deprecated const latestVisibleMoneyRequestAction = getLatestVisibleMoneyRequestAction( reportID, canUserPerformWrite, + // eslint-disable-next-line @typescript-eslint/no-deprecated deprecatedAllSortedReportActions[reportID], visibleReportActionsDataParam, ); From 68a58578851d702a2cddf6191f1c4c1052ef8df0 Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Wed, 15 Apr 2026 08:20:14 +0430 Subject: [PATCH 08/14] fix(lhn): preserve sign in expense preview fallback --- src/libs/OptionsListUtils/index.ts | 24 ++++++++-- tests/unit/OptionsListUtilsTest.tsx | 70 +++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index f999f770eddf..e8a2434ec200 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -10,6 +10,7 @@ import FallbackAvatar from '@assets/images/avatars/fallback-avatar.svg'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import type {PrivateIsArchivedMap} from '@hooks/usePrivateIsArchivedMap'; import {getEnabledCategoriesCount} from '@libs/CategoryUtils'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; import filterArrayByMatch from '@libs/filterArrayByMatch'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {isReportMessageAttachment} from '@libs/isReportMessageAttachment'; @@ -582,6 +583,21 @@ function getLatestVisibleMoneyRequestAction( ); } +function getExpenseReportPreviewText(report: OnyxEntry, moneyRequestAction: OnyxEntry>, translate: LocalizedTranslate): string { + const originalMessage = moneyRequestAction ? getOriginalMessage(moneyRequestAction) : undefined; + const amount = originalMessage?.amount; + const currency = originalMessage?.currency ?? report?.currency; + + if (typeof amount !== 'number' || !currency) { + return ''; + } + + const formattedAmount = convertToDisplayString(amount, currency); + const comment = Parser.htmlToText(originalMessage?.comment ?? '').trim(); + + return formatReportLastMessageText(translate('iou.expenseAmount', formattedAmount, comment || undefined)); +} + function getLastActorDisplayNameFromLastVisibleActions( report: OnyxEntry, lastActorDetails: Partial | null, @@ -972,9 +988,11 @@ function getLastMessageTextForReport({ visibleReportActionsDataParam, ); lastMessageTextFromReport = - formatReportLastMessageText( - Parser.htmlToText(getReportPreviewMessage(report, conciergeReportID, latestVisibleMoneyRequestAction ?? lastReportAction, true, false, null, true)), - ) || lastVisibleMessage?.lastMessageText; + (isExpenseReport(report) && latestVisibleMoneyRequestAction + ? getExpenseReportPreviewText(report, latestVisibleMoneyRequestAction, translate) + : formatReportLastMessageText( + Parser.htmlToText(getReportPreviewMessage(report, conciergeReportID, latestVisibleMoneyRequestAction ?? lastReportAction, true, false, null, true)), + )) || lastVisibleMessage?.lastMessageText; } else if (report?.transactionCount === 0) { lastMessageTextFromReport = translate('report.noActivityYet'); } diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 353a5d4dadc2..9aba15d9d531 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -4136,10 +4136,7 @@ describe('OptionsListUtils', () => { }); }); describe('canonical money request preview fallback', () => { - const getExpectedPreviewText = (report: Report, reportAction: ReportAction, reportPreviewAction?: ReportAction) => - formatReportLastMessageText(Parser.htmlToText(getReportPreviewMessage(report, undefined, reportAction, true, false, null, true, reportPreviewAction))); - - it('should format expense preview without brackets when falling back for expense reports', async () => { + it('should preserve the minus sign when formatting negative expense previews', async () => { const report: Report = { ...createRandomReport(0, undefined), reportID: 'expense-report-1', @@ -4184,7 +4181,7 @@ describe('OptionsListUtils', () => { currentUserLogin: CURRENT_USER_EMAIL, }); - expect(lastMessage).toBe(getExpectedPreviewText(report, moneyRequestAction)); + expect(lastMessage).toBe('-$25.00 for Dinner'); }); it('should ignore deleted money request actions when building canonical expense preview', async () => { @@ -4247,7 +4244,7 @@ describe('OptionsListUtils', () => { currentUserLogin: CURRENT_USER_EMAIL, }); - expect(lastMessage).toBe(getExpectedPreviewText(report, visibleMoneyRequestAction)); + expect(lastMessage).toBe('-$45.00 for Visible comment'); }); it('should format amount-only preview when the canonical money request has an empty comment', async () => { @@ -4295,8 +4292,55 @@ describe('OptionsListUtils', () => { currentUserLogin: CURRENT_USER_EMAIL, }); - expect(lastMessage).toBe(getExpectedPreviewText(report, moneyRequestAction)); - expect(lastMessage.endsWith('$25.00')).toBe(true); + expect(lastMessage).toBe('-$25.00'); + }); + + it('should format zero-value expense previews without adding a minus sign', async () => { + const report: Report = { + ...createRandomReport(0, undefined), + reportID: 'expense-report-zero', + type: CONST.REPORT.TYPE.EXPENSE, + currency: CONST.CURRENCY.USD, + transactionCount: 1, + }; + const createdAction: ReportAction = { + ...createRandomReportAction(14), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + message: [{type: 'COMMENT', text: ''}], + originalMessage: {}, + }; + const moneyRequestAction: ReportAction = { + ...createRandomReportAction(15), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: '2026-04-01 16:00:00.000', + message: [{type: 'COMMENT', text: ''}], + originalMessage: { + amount: 0, + currency: CONST.CURRENCY.USD, + comment: 'Zero amount', + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [createdAction.reportActionID]: createdAction, + [moneyRequestAction.reportActionID]: moneyRequestAction, + }); + await waitForBatchedUpdates(); + + const lastMessage = getLastMessageTextForReport({ + translate: translateLocal, + report, + lastActorDetails: null, + isReportArchived: false, + lastAction: createdAction, + currentUserLogin: CURRENT_USER_EMAIL, + }); + + expect(lastMessage).toBe('$0.00 for Zero amount'); }); it('should format preview correctly for non-USD currencies', async () => { @@ -4344,10 +4388,10 @@ describe('OptionsListUtils', () => { currentUserLogin: CURRENT_USER_EMAIL, }); - expect(lastMessage).toBe(getExpectedPreviewText(report, moneyRequestAction)); + expect(lastMessage).toBe('-€25.00 for Lunch'); }); - it('should fall back to shared preview formatting when the canonical money request is missing amount', async () => { + it('should return an empty preview when the canonical money request is missing amount', async () => { const report: Report = { ...createRandomReport(0, undefined), reportID: 'expense-report-5', @@ -4391,10 +4435,10 @@ describe('OptionsListUtils', () => { currentUserLogin: CURRENT_USER_EMAIL, }); - expect(lastMessage).toBe(getExpectedPreviewText(report, moneyRequestAction)); + expect(lastMessage).toBe(''); }); - it('should fall back to shared preview formatting when the canonical money request is missing currency', async () => { + it('should fall back to the report currency when the canonical money request is missing currency', async () => { const report: Report = { ...createRandomReport(0, undefined), reportID: 'expense-report-6', @@ -4438,7 +4482,7 @@ describe('OptionsListUtils', () => { currentUserLogin: CURRENT_USER_EMAIL, }); - expect(lastMessage).toBe(getExpectedPreviewText(report, moneyRequestAction)); + expect(lastMessage).toBe('-$25.00 for Missing currency'); }); }); it('MOVED_TRANSACTION action', async () => { From 159ec867aa630c370fc5f4fff2ac7d7a1b477b16 Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Mon, 11 May 2026 16:44:48 +0430 Subject: [PATCH 09/14] skip money-request fallback when only CREATED remains --- src/libs/OptionsListUtils/index.ts | 13 ++++++----- tests/unit/OptionsListUtilsTest.tsx | 35 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 20e138aa83bc..fbd67412f976 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -983,12 +983,15 @@ function getLastMessageTextForReport({ deprecatedAllSortedReportActions[reportID], visibleReportActionsDataParam, ); - lastMessageTextFromReport = - (isExpenseReport(report) && latestVisibleMoneyRequestAction + if (latestVisibleMoneyRequestAction) { + lastMessageTextFromReport = isExpenseReport(report) ? getExpenseReportPreviewText(report, latestVisibleMoneyRequestAction, translate) - : formatReportLastMessageText( - Parser.htmlToText(getReportPreviewMessage(report, conciergeReportID, latestVisibleMoneyRequestAction ?? lastReportAction, true, false, null, true)), - )) || lastVisibleMessage?.lastMessageText; + : formatReportLastMessageText(Parser.htmlToText(getReportPreviewMessage(report, conciergeReportID, latestVisibleMoneyRequestAction, true, false, null, true))); + } else if (!isCreatedAction(lastReportAction)) { + lastMessageTextFromReport = + formatReportLastMessageText(Parser.htmlToText(getReportPreviewMessage(report, conciergeReportID, lastReportAction, true, false, null, true))) || + lastVisibleMessage?.lastMessageText; + } } else if (report?.transactionCount === 0) { lastMessageTextFromReport = translate('report.noActivityYet'); } diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index a032e87e1774..2843ed3a659c 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -4425,6 +4425,41 @@ describe('OptionsListUtils', () => { expect(lastMessage).toBe(''); }); + it('should return an empty preview when only a created action is visible after expense deletion', async () => { + const report: Report = { + ...createRandomReport(0, undefined), + reportID: 'expense-report-deleted', + type: CONST.REPORT.TYPE.EXPENSE, + currency: CONST.CURRENCY.USD, + transactionCount: 1, + lastMessageText: '-$25.00 for Deleted expense', + }; + const createdAction: ReportAction = { + ...createRandomReportAction(16), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + message: [{type: 'COMMENT', text: ''}], + originalMessage: {}, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [createdAction.reportActionID]: createdAction, + }); + await waitForBatchedUpdates(); + + const lastMessage = getLastMessageTextForReport({ + translate: translateLocal, + report, + lastActorDetails: null, + isReportArchived: false, + lastAction: createdAction, + currentUserLogin: CURRENT_USER_EMAIL, + }); + + expect(lastMessage).toBe(''); + }); + it('should fall back to the report currency when the canonical money request is missing currency', async () => { const report: Report = { ...createRandomReport(0, undefined), From 49cf77eb92a5f914df2f5ed84ffa570aafd6f667 Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Tue, 12 May 2026 08:45:32 +0430 Subject: [PATCH 10/14] add test for formatting non-expense IOU fallback with requestor name --- tests/unit/OptionsListUtilsTest.tsx | 61 +++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 2843ed3a659c..a0cfbeafe8ca 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -4460,6 +4460,67 @@ describe('OptionsListUtils', () => { expect(lastMessage).toBe(''); }); + it('should format non-expense IOU fallback with the requestor name when the latest visible action is not an IOU action', async () => { + const report: Report = { + ...createRandomReport(0, undefined), + reportID: 'iou-report-created-last-action', + type: CONST.REPORT.TYPE.IOU, + currency: CONST.CURRENCY.USD, + transactionCount: 1, + ownerAccountID: 1, + managerID: CURRENT_USER_ACCOUNT_ID, + isWaitingOnBankAccount: false, + }; + const createdAction: ReportAction = { + ...createRandomReportAction(17), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + created: '2026-04-01 09:00:00.000', + message: [{type: 'COMMENT', text: ''}], + originalMessage: {}, + }; + const transaction: Transaction = { + ...createRandomTransaction(1), + amount: 2500, + currency: CONST.CURRENCY.USD, + merchant: 'Coffee', + modifiedMerchant: '', + }; + const moneyRequestAction: ReportAction = { + ...createRandomReportAction(18), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + actorAccountID: 1, + created: '2026-04-01 10:00:00.000', + message: [{type: 'COMMENT', text: ''}], + originalMessage: { + amount: -2500, + currency: CONST.CURRENCY.USD, + IOUTransactionID: transaction.transactionID, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [createdAction.reportActionID]: createdAction, + [moneyRequestAction.reportActionID]: moneyRequestAction, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); + await waitForBatchedUpdates(); + + const lastMessage = getLastMessageTextForReport({ + translate: translateLocal, + report, + lastActorDetails: null, + isReportArchived: false, + lastAction: createdAction, + currentUserLogin: CURRENT_USER_EMAIL, + }); + + expect(lastMessage).toBe('Mister Fantastic: $25.00 for Coffee'); + }); + it('should fall back to the report currency when the canonical money request is missing currency', async () => { const report: Report = { ...createRandomReport(0, undefined), From 76b1fc65a2fee2628dd55ac1d8a16e201efd7697 Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Tue, 12 May 2026 13:48:37 +0430 Subject: [PATCH 11/14] fix: preserve IOU preview fallback behavior --- src/libs/OptionsListUtils/index.ts | 8 ++++---- tests/unit/OptionsListUtilsTest.tsx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index bd284a224a49..865cf462c791 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -984,10 +984,10 @@ function getLastMessageTextForReport({ deprecatedAllSortedReportActions[reportID], visibleReportActionsDataParam, ); - if (latestVisibleMoneyRequestAction) { - lastMessageTextFromReport = isExpenseReport(report) - ? getExpenseReportPreviewText(report, latestVisibleMoneyRequestAction, translate) - : formatReportLastMessageText(Parser.htmlToText(getReportPreviewMessage(report, conciergeReportID, latestVisibleMoneyRequestAction, true, false, null, true))); + if (isExpenseReport(report) && latestVisibleMoneyRequestAction) { + lastMessageTextFromReport = getExpenseReportPreviewText(report, latestVisibleMoneyRequestAction, translate); + } else if (!isExpenseReport(report)) { + lastMessageTextFromReport = lastVisibleMessage?.lastMessageText; } else if (!isCreatedAction(lastReportAction)) { lastMessageTextFromReport = formatReportLastMessageText(Parser.htmlToText(getReportPreviewMessage(report, conciergeReportID, lastReportAction, true, false, null, true))) || diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index a0cfbeafe8ca..48de9f542bf6 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -4460,7 +4460,7 @@ describe('OptionsListUtils', () => { expect(lastMessage).toBe(''); }); - it('should format non-expense IOU fallback with the requestor name when the latest visible action is not an IOU action', async () => { + it('should preserve last visible message fallback for non-expense IOU reports', async () => { const report: Report = { ...createRandomReport(0, undefined), reportID: 'iou-report-created-last-action', @@ -4518,7 +4518,7 @@ describe('OptionsListUtils', () => { currentUserLogin: CURRENT_USER_EMAIL, }); - expect(lastMessage).toBe('Mister Fantastic: $25.00 for Coffee'); + expect(lastMessage).toBe(''); }); it('should fall back to the report currency when the canonical money request is missing currency', async () => { From 524f5c582d4381313bf5f63f5f8eacfcce5a1187 Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Tue, 12 May 2026 14:34:33 +0430 Subject: [PATCH 12/14] preserve merchant in expense preview fallback --- src/libs/OptionsListUtils/index.ts | 17 +++++++-- tests/unit/OptionsListUtilsTest.tsx | 57 +++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 865cf462c791..da59fc8dd6a3 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -166,7 +166,7 @@ import { } from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils'; -import {isScanning} from '@libs/TransactionUtils'; +import {getMerchantOrDescription, isScanning} from '@libs/TransactionUtils'; import {generateAccountID} from '@libs/UserUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -186,6 +186,7 @@ import type { ReportActions, ReportAttributesDerivedValue, ReportMetadata, + Transaction, VisibleReportActionsDerivedValue, } from '@src/types/onyx'; import type {Attendee, Participant} from '@src/types/onyx/IOU'; @@ -574,7 +575,12 @@ function getLatestVisibleMoneyRequestAction( ); } -function getExpenseReportPreviewText(report: OnyxEntry, moneyRequestAction: OnyxEntry>, translate: LocalizedTranslate): string { +function getExpenseReportPreviewText( + report: OnyxEntry, + moneyRequestAction: OnyxEntry>, + translate: LocalizedTranslate, + transactions: Transaction[] = [], +): string { const originalMessage = moneyRequestAction ? getOriginalMessage(moneyRequestAction) : undefined; const amount = originalMessage?.amount; const currency = originalMessage?.currency ?? report?.currency; @@ -584,7 +590,10 @@ function getExpenseReportPreviewText(report: OnyxEntry, moneyRequestActi } const formattedAmount = convertToDisplayString(amount, currency); - const comment = Parser.htmlToText(originalMessage?.comment ?? '').trim(); + const linkedTransaction = transactions.find((transaction) => transaction.transactionID === originalMessage?.IOUTransactionID); + const merchantOrDescription = linkedTransaction ? getMerchantOrDescription(linkedTransaction) : ''; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const comment = Parser.htmlToText(merchantOrDescription || originalMessage?.comment || '').trim(); return formatReportLastMessageText(translate('iou.expenseAmount', formattedAmount, comment || undefined)); } @@ -985,7 +994,7 @@ function getLastMessageTextForReport({ visibleReportActionsDataParam, ); if (isExpenseReport(report) && latestVisibleMoneyRequestAction) { - lastMessageTextFromReport = getExpenseReportPreviewText(report, latestVisibleMoneyRequestAction, translate); + lastMessageTextFromReport = getExpenseReportPreviewText(report, latestVisibleMoneyRequestAction, translate, transactions); } else if (!isExpenseReport(report)) { lastMessageTextFromReport = lastVisibleMessage?.lastMessageText; } else if (!isCreatedAction(lastReportAction)) { diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 48de9f542bf6..dedc74658f3d 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -4330,6 +4330,63 @@ describe('OptionsListUtils', () => { expect(lastMessage).toBe('$0.00 for Zero amount'); }); + it('should use the linked transaction merchant when the canonical money request has no comment', async () => { + const report: Report = { + ...createRandomReport(0, undefined), + reportID: 'expense-report-merchant-fallback', + type: CONST.REPORT.TYPE.EXPENSE, + currency: CONST.CURRENCY.USD, + transactionCount: 1, + }; + const createdAction: ReportAction = { + ...createRandomReportAction(19), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + message: [{type: 'COMMENT', text: ''}], + originalMessage: {}, + }; + const transaction: Transaction = { + ...createRandomTransaction(2), + reportID: report.reportID, + amount: 0, + currency: CONST.CURRENCY.USD, + merchant: 'Coffee', + modifiedMerchant: '', + }; + const moneyRequestAction: ReportAction = { + ...createRandomReportAction(20), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: '2026-04-01 17:00:00.000', + message: [{type: 'COMMENT', text: ''}], + originalMessage: { + amount: 0, + currency: CONST.CURRENCY.USD, + IOUTransactionID: transaction.transactionID, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [createdAction.reportActionID]: createdAction, + [moneyRequestAction.reportActionID]: moneyRequestAction, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); + await waitForBatchedUpdates(); + + const lastMessage = getLastMessageTextForReport({ + translate: translateLocal, + report, + lastActorDetails: null, + isReportArchived: false, + lastAction: createdAction, + currentUserLogin: CURRENT_USER_EMAIL, + }); + + expect(lastMessage).toBe('$0.00 for Coffee'); + }); + it('should format preview correctly for non-USD currencies', async () => { const report: Report = { ...createRandomReport(0, undefined), From 33ace78f158ba16cbbeeea587350d9a9af95b7d1 Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Wed, 13 May 2026 19:03:05 +0430 Subject: [PATCH 13/14] fix: use linked transaction amount in expense preview --- src/libs/OptionsListUtils/index.ts | 8 ++-- tests/unit/OptionsListUtilsTest.tsx | 58 +++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index da59fc8dd6a3..9f4912486c5c 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -166,7 +166,7 @@ import { } from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils'; -import {getMerchantOrDescription, isScanning} from '@libs/TransactionUtils'; +import {getMerchantOrDescription, getAmount as getTransactionAmount, getCurrency as getTransactionCurrency, isScanning} from '@libs/TransactionUtils'; import {generateAccountID} from '@libs/UserUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -582,15 +582,15 @@ function getExpenseReportPreviewText( transactions: Transaction[] = [], ): string { const originalMessage = moneyRequestAction ? getOriginalMessage(moneyRequestAction) : undefined; - const amount = originalMessage?.amount; - const currency = originalMessage?.currency ?? report?.currency; + const linkedTransaction = transactions.find((transaction) => transaction.transactionID === originalMessage?.IOUTransactionID); + const amount = linkedTransaction ? getTransactionAmount(linkedTransaction, true) : originalMessage?.amount; + const currency = linkedTransaction ? getTransactionCurrency(linkedTransaction) : (originalMessage?.currency ?? report?.currency); if (typeof amount !== 'number' || !currency) { return ''; } const formattedAmount = convertToDisplayString(amount, currency); - const linkedTransaction = transactions.find((transaction) => transaction.transactionID === originalMessage?.IOUTransactionID); const merchantOrDescription = linkedTransaction ? getMerchantOrDescription(linkedTransaction) : ''; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const comment = Parser.htmlToText(merchantOrDescription || originalMessage?.comment || '').trim(); diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index dedc74658f3d..293b90c328cc 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -4387,6 +4387,64 @@ describe('OptionsListUtils', () => { expect(lastMessage).toBe('$0.00 for Coffee'); }); + it('should use the linked transaction amount when the canonical money request amount is stale', async () => { + const report: Report = { + ...createRandomReport(0, undefined), + reportID: 'expense-report-edited-amount', + type: CONST.REPORT.TYPE.EXPENSE, + currency: CONST.CURRENCY.USD, + transactionCount: 1, + }; + const createdAction: ReportAction = { + ...createRandomReportAction(21), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + message: [{type: 'COMMENT', text: ''}], + originalMessage: {}, + }; + const transaction: Transaction = { + ...createRandomTransaction(3), + reportID: report.reportID, + amount: 1000, + modifiedAmount: 2500, + currency: CONST.CURRENCY.USD, + merchant: 'Coffee', + modifiedMerchant: '', + }; + const moneyRequestAction: ReportAction = { + ...createRandomReportAction(22), + reportID: report.reportID, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + created: '2026-04-01 18:00:00.000', + message: [{type: 'COMMENT', text: ''}], + originalMessage: { + amount: -1000, + currency: CONST.CURRENCY.USD, + IOUTransactionID: transaction.transactionID, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [createdAction.reportActionID]: createdAction, + [moneyRequestAction.reportActionID]: moneyRequestAction, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); + await waitForBatchedUpdates(); + + const lastMessage = getLastMessageTextForReport({ + translate: translateLocal, + report, + lastActorDetails: null, + isReportArchived: false, + lastAction: createdAction, + currentUserLogin: CURRENT_USER_EMAIL, + }); + + expect(lastMessage).toBe('-$25.00 for Coffee'); + }); + it('should format preview correctly for non-USD currencies', async () => { const report: Report = { ...createRandomReport(0, undefined), From 3779b4016057818535f3e4fa69ae752de3a93b36 Mon Sep 17 00:00:00 2001 From: marufsharifi Date: Tue, 19 May 2026 14:45:14 +0430 Subject: [PATCH 14/14] fix: update expense preview to use description instead of merchant --- src/libs/OptionsListUtils/index.ts | 6 +- tests/unit/OptionsListUtilsTest.tsx | 115 ---------------------------- 2 files changed, 3 insertions(+), 118 deletions(-) diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index d756353112a3..45d107bce161 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -166,7 +166,7 @@ import { } from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import {getTaskCreatedMessage, getTaskReportActionMessage} from '@libs/TaskUtils'; -import {getMerchantOrDescription, getAmount as getTransactionAmount, getCurrency as getTransactionCurrency, isScanning} from '@libs/TransactionUtils'; +import {getDescription, getAmount as getTransactionAmount, getCurrency as getTransactionCurrency, isScanning} from '@libs/TransactionUtils'; import {generateAccountID} from '@libs/UserUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -591,9 +591,9 @@ function getExpenseReportPreviewText( } const formattedAmount = convertToDisplayString(amount, currency); - const merchantOrDescription = linkedTransaction ? getMerchantOrDescription(linkedTransaction) : ''; + const description = linkedTransaction ? getDescription(linkedTransaction) : ''; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const comment = Parser.htmlToText(merchantOrDescription || originalMessage?.comment || '').trim(); + const comment = Parser.htmlToText(description || originalMessage?.comment || '').trim(); return formatReportLastMessageText(translate('iou.expenseAmount', formattedAmount, comment || undefined)); } diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index d872c3993e2b..b8f36082d214 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -4331,121 +4331,6 @@ describe('OptionsListUtils', () => { expect(lastMessage).toBe('$0.00 for Zero amount'); }); - it('should use the linked transaction merchant when the canonical money request has no comment', async () => { - const report: Report = { - ...createRandomReport(0, undefined), - reportID: 'expense-report-merchant-fallback', - type: CONST.REPORT.TYPE.EXPENSE, - currency: CONST.CURRENCY.USD, - transactionCount: 1, - }; - const createdAction: ReportAction = { - ...createRandomReportAction(19), - reportID: report.reportID, - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - message: [{type: 'COMMENT', text: ''}], - originalMessage: {}, - }; - const transaction: Transaction = { - ...createRandomTransaction(2), - reportID: report.reportID, - amount: 0, - currency: CONST.CURRENCY.USD, - merchant: 'Coffee', - modifiedMerchant: '', - }; - const moneyRequestAction: ReportAction = { - ...createRandomReportAction(20), - reportID: report.reportID, - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - created: '2026-04-01 17:00:00.000', - message: [{type: 'COMMENT', text: ''}], - originalMessage: { - amount: 0, - currency: CONST.CURRENCY.USD, - IOUTransactionID: transaction.transactionID, - type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, - }, - }; - - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { - [createdAction.reportActionID]: createdAction, - [moneyRequestAction.reportActionID]: moneyRequestAction, - }); - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); - await waitForBatchedUpdates(); - - const lastMessage = getLastMessageTextForReport({ - translate: translateLocal, - report, - lastActorDetails: null, - isReportArchived: false, - lastAction: createdAction, - currentUserLogin: CURRENT_USER_EMAIL, - }); - - expect(lastMessage).toBe('$0.00 for Coffee'); - }); - - it('should use the linked transaction amount when the canonical money request amount is stale', async () => { - const report: Report = { - ...createRandomReport(0, undefined), - reportID: 'expense-report-edited-amount', - type: CONST.REPORT.TYPE.EXPENSE, - currency: CONST.CURRENCY.USD, - transactionCount: 1, - }; - const createdAction: ReportAction = { - ...createRandomReportAction(21), - reportID: report.reportID, - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - message: [{type: 'COMMENT', text: ''}], - originalMessage: {}, - }; - const transaction: Transaction = { - ...createRandomTransaction(3), - reportID: report.reportID, - amount: 1000, - modifiedAmount: 2500, - currency: CONST.CURRENCY.USD, - merchant: 'Coffee', - modifiedMerchant: '', - }; - const moneyRequestAction: ReportAction = { - ...createRandomReportAction(22), - reportID: report.reportID, - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - created: '2026-04-01 18:00:00.000', - message: [{type: 'COMMENT', text: ''}], - originalMessage: { - amount: -1000, - currency: CONST.CURRENCY.USD, - IOUTransactionID: transaction.transactionID, - type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, - }, - }; - - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { - [createdAction.reportActionID]: createdAction, - [moneyRequestAction.reportActionID]: moneyRequestAction, - }); - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); - await waitForBatchedUpdates(); - - const lastMessage = getLastMessageTextForReport({ - translate: translateLocal, - report, - lastActorDetails: null, - isReportArchived: false, - lastAction: createdAction, - currentUserLogin: CURRENT_USER_EMAIL, - }); - - expect(lastMessage).toBe('-$25.00 for Coffee'); - }); - it('should format preview correctly for non-USD currencies', async () => { const report: Report = { ...createRandomReport(0, undefined),