From 72762ce891a52b48f84b34fe7eb7289dff79bd32 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= <marco@expensify.com>
Date: Thu, 22 Jan 2026 14:51:30 -0500
Subject: [PATCH 01/16] fix: migrate ReportActionItem to useOnyx for policyTags

This properly migrates the ReportActionItem component to use the useOnyx
hook for policyTags data instead of relying on the deprecated module-level
Onyx.connect pattern in ModifiedExpenseMessage.ts.

Changes:
- Import getForReportActionTemp instead of getForReportAction
- Add useLocalize hook to get translate function
- Add useOnyx hook to fetch policyTags for the reports policy
- Pass translate, policy, and policyTags to getForReportActionTemp
- Add eslint-disable comments for remaining Onyx.connectWithoutView calls
  (used by utility files that will be migrated in follow-up PRs)
---
 src/libs/ModifiedExpenseMessage.ts          |  2 ++
 src/pages/inbox/report/ReportActionItem.tsx | 11 +++++++----
 2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index c23c05bbc2b4..cf534f481157 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -28,6 +28,7 @@ import {getPolicyName, getReportName, getRootParentReport, isPolicyExpenseChat,
 import {getFormattedAttendees, getTagArrayFromName} from './TransactionUtils';
 
 let allPolicyTags: OnyxCollection<PolicyTagLists> = {};
+// eslint-disable-next-line @typescript-eslint/no-deprecated -- This Onyx.connect is used by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that will be migrated in follow-up PRs
 Onyx.connectWithoutView({
     key: ONYXKEYS.COLLECTION.POLICY_TAGS,
     waitForCollectionCallback: true,
@@ -44,6 +45,7 @@ let environmentURL: string;
 getEnvironmentURL().then((url: string) => (environmentURL = url));
 
 let currentUserLogin = '';
+// eslint-disable-next-line @typescript-eslint/no-deprecated -- This Onyx.connect is used by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that will be migrated in follow-up PRs
 Onyx.connectWithoutView({
     key: ONYXKEYS.SESSION,
     callback: (value) => {
diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx
index a8dece8f1384..e37013c4fb1b 100644
--- a/src/pages/inbox/report/ReportActionItem.tsx
+++ b/src/pages/inbox/report/ReportActionItem.tsx
@@ -7,7 +7,7 @@ import useOnyx from '@hooks/useOnyx';
 import useOriginalReportID from '@hooks/useOriginalReportID';
 import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
 import useReportIsArchived from '@hooks/useReportIsArchived';
-import {getForReportAction, getMovedReportID} from '@libs/ModifiedExpenseMessage';
+import {getForReportActionTemp, getMovedReportID} from '@libs/ModifiedExpenseMessage';
 import {getIOUReportIDFromReportActionPreview, getOriginalMessage} from '@libs/ReportActionsUtils';
 import {
     chatIncludesChronosWithID,
@@ -100,6 +100,8 @@ function ReportActionItem({
     const originalReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`];
     const isOriginalReportArchived = useReportIsArchived(originalReportID);
     const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
+    const {translate} = useLocalize();
+    const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: true});
     const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
     const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true});
     const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {canBeMissing: true});
@@ -162,12 +164,13 @@ function ReportActionItem({
                 action as OnyxEntry<ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENT_DEQUEUED | typeof CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENT_ACH_CANCELED>>,
                 report,
             )}
-            modifiedExpenseMessage={getForReportAction({
+            modifiedExpenseMessage={getForReportActionTemp({
+                translate,
                 reportAction: action,
-                policyID: report?.policyID,
+                policy,
                 movedFromReport,
                 movedToReport,
-                policyForMovingExpensesID,
+                policyTags,
             })}
             getTransactionsWithReceipts={getTransactionsWithReceipts}
             clearError={clearError}

From d0ff5e234488763c6bc06aad41c04f7da168fc7f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= <marco@expensify.com>
Date: Thu, 22 Jan 2026 14:55:12 -0500
Subject: [PATCH 02/16] fix: improve eslint-disable comments with more specific
 justification

---
 src/libs/ModifiedExpenseMessage.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index cf534f481157..8af66ecfaa2f 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -28,7 +28,7 @@ import {getPolicyName, getReportName, getRootParentReport, isPolicyExpenseChat,
 import {getFormattedAttendees, getTagArrayFromName} from './TransactionUtils';
 
 let allPolicyTags: OnyxCollection<PolicyTagLists> = {};
-// eslint-disable-next-line @typescript-eslint/no-deprecated -- This Onyx.connect is used by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that will be migrated in follow-up PRs
+// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage utility uses Onyx.connectWithoutView to maintain global policyTags state for getForReportAction, which is called by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that cannot use hooks. This will be migrated when those utility files are refactored.
 Onyx.connectWithoutView({
     key: ONYXKEYS.COLLECTION.POLICY_TAGS,
     waitForCollectionCallback: true,
@@ -45,7 +45,7 @@ let environmentURL: string;
 getEnvironmentURL().then((url: string) => (environmentURL = url));
 
 let currentUserLogin = '';
-// eslint-disable-next-line @typescript-eslint/no-deprecated -- This Onyx.connect is used by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that will be migrated in follow-up PRs
+// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage utility uses Onyx.connectWithoutView to access session data globally for getForReportAction, which is called by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that cannot use hooks. This will be migrated when those utility files are refactored.
 Onyx.connectWithoutView({
     key: ONYXKEYS.SESSION,
     callback: (value) => {

From 58e8860562dc628929322eb95104c54a74f26c0b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= <marco@expensify.com>
Date: Thu, 22 Jan 2026 15:37:00 -0500
Subject: [PATCH 03/16] fix: add currentUserLogin parameter to
 getForReportActionTemp

The function was still using module-level currentUserLogin from Onyx.connect.
Now it accepts currentUserLogin as a parameter for full migration.

Changes:
- Add currentUserLogin parameter to getForReportActionTemp
- Update ReportActionItem.tsx to pass currentUserLogin from session
- Update ContextMenuActions.tsx to pass currentUserLogin
- Update BaseReportActionContextMenu.tsx to fetch session and pass currentUserLogin
---
 Mobile-Expensify                                          | 2 +-
 src/libs/ModifiedExpenseMessage.ts                        | 4 +++-
 src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx | 1 +
 src/pages/inbox/report/ReportActionItem.tsx               | 2 ++
 4 files changed, 7 insertions(+), 2 deletions(-)

diff --git a/Mobile-Expensify b/Mobile-Expensify
index a92777fbadfc..0d187767eb98 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit a92777fbadfc660e19508d03464e4355324ce4d4
+Subproject commit 0d187767eb984c387a981f45902ff285d67b76ff
diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index 8af66ecfaa2f..2e847a291d25 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -516,6 +516,7 @@ function getForReportActionTemp({
     movedFromReport,
     movedToReport,
     policyTags,
+    currentUserLogin: currentUserLoginParam,
 }: {
     translate: LocalizedTranslate;
     reportAction: OnyxEntry<ReportAction>;
@@ -523,6 +524,7 @@ function getForReportActionTemp({
     movedFromReport?: OnyxEntry<Report>;
     movedToReport?: OnyxEntry<Report>;
     policyTags: OnyxEntry<PolicyTagLists>;
+    currentUserLogin: string;
 }): string {
     if (!isModifiedExpenseAction(reportAction)) {
         return '';
@@ -612,7 +614,7 @@ function getForReportActionTemp({
         if (reportActionOriginalMessage?.source === CONST.CATEGORY_SOURCE.AI) {
             categoryLabel += ` ${translate('iou.basedOnAI')}`;
         } else if (reportActionOriginalMessage?.source === CONST.CATEGORY_SOURCE.MCC) {
-            const isAdmin = isPolicyAdmin(policy, currentUserLogin);
+            const isAdmin = isPolicyAdmin(policy, currentUserLoginParam);
 
             // For admins, create a hyperlink to the workspace rules page
             if (isAdmin && policy?.id) {
diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
index c7c4cfa6aeb8..8fe9824840c4 100644
--- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
@@ -739,6 +739,7 @@ const ContextMenuActions: ContextMenuAction[] = [
                         movedFromReport,
                         movedToReport,
                         policyTags,
+                        currentUserLogin: currentUserPersonalDetails?.login ?? '',
                     });
                     Clipboard.setString(modifyExpenseMessage);
                 } else if (isReimbursementDeQueuedOrCanceledAction(reportAction)) {
diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx
index e37013c4fb1b..a4eaa79db6cf 100644
--- a/src/pages/inbox/report/ReportActionItem.tsx
+++ b/src/pages/inbox/report/ReportActionItem.tsx
@@ -102,6 +102,7 @@ function ReportActionItem({
     const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
     const {translate} = useLocalize();
     const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: true});
+    const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true});
     const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
     const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true});
     const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {canBeMissing: true});
@@ -171,6 +172,7 @@ function ReportActionItem({
                 movedFromReport,
                 movedToReport,
                 policyTags,
+                currentUserLogin: session?.email ?? '',
             })}
             getTransactionsWithReceipts={getTransactionsWithReceipts}
             clearError={clearError}

From b728cbed65a0dd99d6d4220e1efb02434956147e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= <marco@expensify.com>
Date: Thu, 22 Jan 2026 16:22:49 -0500
Subject: [PATCH 04/16] fix: add currentUserLogin to destructuring in copy to
 clipboard action

---
 src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
index 8fe9824840c4..387bce52223d 100644
--- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
@@ -714,6 +714,7 @@ const ContextMenuActions: ContextMenuAction[] = [
                 policyTags,
                 translate,
                 harvestReport,
+                currentUserLogin,
             },
         ) => {
             const isReportPreviewAction = isReportPreviewActionReportActionsUtils(reportAction);

From 0821bd14adcef674e9f6b56e62f1471a80c20bf5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= <marco@expensify.com>
Date: Thu, 22 Jan 2026 17:22:59 -0500
Subject: [PATCH 05/16] fix: remove unused policyForMovingExpensesID

---
 src/pages/inbox/report/ReportActionItem.tsx | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx
index a4eaa79db6cf..a47b2ec66284 100644
--- a/src/pages/inbox/report/ReportActionItem.tsx
+++ b/src/pages/inbox/report/ReportActionItem.tsx
@@ -5,7 +5,6 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
 import useLocalize from '@hooks/useLocalize';
 import useOnyx from '@hooks/useOnyx';
 import useOriginalReportID from '@hooks/useOriginalReportID';
-import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
 import useReportIsArchived from '@hooks/useReportIsArchived';
 import {getForReportActionTemp, getMovedReportID} from '@libs/ModifiedExpenseMessage';
 import {getIOUReportIDFromReportActionPreview, getOriginalMessage} from '@libs/ReportActionsUtils';
@@ -112,7 +111,6 @@ function ReportActionItem({
     const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`];
     const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true});
     const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true});
-    const {policyForMovingExpensesID} = usePolicyForMovingExpenses();
     // The app would crash due to subscribing to the entire report collection if parentReportID is an empty string. So we should have a fallback ID here.
     // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
     const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID || undefined}`];

From f8f7357f66b5e45d29388b820d5be2e81ef69719 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= <marco@expensify.com>
Date: Thu, 22 Jan 2026 17:46:52 -0500
Subject: [PATCH 06/16] chore: trigger CI checks


From 128592e02c87174dbcba71effc5482759983793e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= <marco@expensify.com>
Date: Tue, 3 Feb 2026 20:23:06 -0600
Subject: [PATCH 07/16] fix: address PR review comments

- Use useCurrentUserPersonalDetails hook for currentUserLogin instead of session
- Use policyForMovingExpensesID when policyID is fake for policy tags
- Update ESLint disable comments with more specific justifications
---
 src/libs/ModifiedExpenseMessage.ts          |  4 ++--
 src/pages/inbox/report/ReportActionItem.tsx | 12 +++++++-----
 2 files changed, 9 insertions(+), 7 deletions(-)

diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index 2e847a291d25..90aa5e4dc8a2 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -28,7 +28,7 @@ import {getPolicyName, getReportName, getRootParentReport, isPolicyExpenseChat,
 import {getFormattedAttendees, getTagArrayFromName} from './TransactionUtils';
 
 let allPolicyTags: OnyxCollection<PolicyTagLists> = {};
-// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage utility uses Onyx.connectWithoutView to maintain global policyTags state for getForReportAction, which is called by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that cannot use hooks. This will be migrated when those utility files are refactored.
+// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage.getForReportAction is called by utility files (ReportUtils.getModifiedExpenseMessage, OptionsListUtils, ReportNameUtils) that cannot use React hooks. The global policyTags state is required for these non-React contexts. Migration to useOnyx will happen when these utility files are converted to custom hooks.
 Onyx.connectWithoutView({
     key: ONYXKEYS.COLLECTION.POLICY_TAGS,
     waitForCollectionCallback: true,
@@ -45,7 +45,7 @@ let environmentURL: string;
 getEnvironmentURL().then((url: string) => (environmentURL = url));
 
 let currentUserLogin = '';
-// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage utility uses Onyx.connectWithoutView to access session data globally for getForReportAction, which is called by utility files (ReportUtils, OptionsListUtils, ReportNameUtils) that cannot use hooks. This will be migrated when those utility files are refactored.
+// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage.getForReportAction requires currentUserLogin for isPolicyAdmin checks. This function is called by non-React utility files (ReportUtils.getModifiedExpenseMessage, OptionsListUtils, ReportNameUtils) that cannot use React hooks. Migration to useOnyx will happen when these utility files are converted to custom hooks.
 Onyx.connectWithoutView({
     key: ONYXKEYS.SESSION,
     callback: (value) => {
diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx
index a47b2ec66284..67bff38f8ab5 100644
--- a/src/pages/inbox/report/ReportActionItem.tsx
+++ b/src/pages/inbox/report/ReportActionItem.tsx
@@ -5,6 +5,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
 import useLocalize from '@hooks/useLocalize';
 import useOnyx from '@hooks/useOnyx';
 import useOriginalReportID from '@hooks/useOriginalReportID';
+import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses';
 import useReportIsArchived from '@hooks/useReportIsArchived';
 import {getForReportActionTemp, getMovedReportID} from '@libs/ModifiedExpenseMessage';
 import {getIOUReportIDFromReportActionPreview, getOriginalMessage} from '@libs/ReportActionsUtils';
@@ -98,10 +99,11 @@ function ReportActionItem({
     const originalReportID = useOriginalReportID(reportID, action);
     const originalReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`];
     const isOriginalReportArchived = useReportIsArchived(originalReportID);
-    const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
-    const {translate} = useLocalize();
-    const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: true});
-    const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true});
+    const {accountID: currentUserAccountID, login: currentUserLogin} = useCurrentUserPersonalDetails();
+    const {policyForMovingExpensesID} = usePolicyForMovingExpenses();
+    // Use policyForMovingExpensesID when policyID is fake to ensure tag edits on moved expenses use the correct workspace's tag lists
+    const policyIDForTags = report?.policyID === CONST.POLICY.OWNER_EMAIL_FAKE && policyForMovingExpensesID ? policyForMovingExpensesID : report?.policyID;
+    const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyIDForTags}`, {canBeMissing: true});
     const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
     const [allTransactionDrafts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {canBeMissing: true});
     const [reportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, {canBeMissing: true});
@@ -170,7 +172,7 @@ function ReportActionItem({
                 movedFromReport,
                 movedToReport,
                 policyTags,
-                currentUserLogin: session?.email ?? '',
+                currentUserLogin: currentUserLogin ?? '',
             })}
             getTransactionsWithReceipts={getTransactionsWithReceipts}
             clearError={clearError}

From 14255aa6b041b081733ecce84df86c58a567d730 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= <marco@expensify.com>
Date: Tue, 3 Feb 2026 20:42:17 -0600
Subject: [PATCH 08/16] fix: revert Mobile-Expensify submodule change

Reverts accidental submodule pointer change from rebase.
---
 Mobile-Expensify | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Mobile-Expensify b/Mobile-Expensify
index 0d187767eb98..a92777fbadfc 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 0d187767eb984c387a981f45902ff285d67b76ff
+Subproject commit a92777fbadfc660e19508d03464e4355324ce4d4

From acb30b74700b0cfcd99674b404996754ba469de1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= <marco@expensify.com>
Date: Tue, 3 Feb 2026 20:54:07 -0600
Subject: [PATCH 09/16] fix: TypeScript error in ContextMenuActions

Use currentUserPersonalDetails in destructuring instead of currentUserLogin.
---
 src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
index 387bce52223d..b01f6f39761c 100644
--- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
@@ -714,7 +714,7 @@ const ContextMenuActions: ContextMenuAction[] = [
                 policyTags,
                 translate,
                 harvestReport,
-                currentUserLogin,
+                currentUserPersonalDetails,
             },
         ) => {
             const isReportPreviewAction = isReportPreviewActionReportActionsUtils(reportAction);

From be64c7f8692f09615d96f301d51bef0149f8f63e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= <marco@expensify.com>
Date: Wed, 4 Feb 2026 16:00:53 -0600
Subject: [PATCH 10/16] test: add unit tests for getForReportActionTemp

Add comprehensive unit tests for getForReportActionTemp function to ensure
it behaves the same as getForReportAction. These tests cover:
- Amount changes
- Description changes
- Merchant set/remove
- Category changes with AI/MCC attribution
- Distance changes
- Moving expenses
- Edge cases (non-modified-expense actions, empty messages)

Once the migration from getForReportAction to getForReportActionTemp is
complete, the tests for getForReportAction can be safely removed.
---
 tests/unit/ModifiedExpenseMessageTest.ts | 301 ++++++++++++++++++++++-
 1 file changed, 300 insertions(+), 1 deletion(-)

diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts
index 962e2cbe2592..0ed4b258ae19 100644
--- a/tests/unit/ModifiedExpenseMessageTest.ts
+++ b/tests/unit/ModifiedExpenseMessageTest.ts
@@ -1,5 +1,5 @@
 import {getEnvironmentURL} from '@libs/Environment/Environment';
-import {getForReportAction, getMovedFromOrToReportMessage, getMovedReportID} from '@libs/ModifiedExpenseMessage';
+import {getForReportAction, getForReportActionTemp, getMovedFromOrToReportMessage, getMovedReportID} from '@libs/ModifiedExpenseMessage';
 // eslint-disable-next-line no-restricted-syntax -- this is required to allow mocking
 import * as PolicyUtils from '@libs/PolicyUtils';
 // eslint-disable-next-line no-restricted-syntax -- this is required to allow mocking
@@ -952,4 +952,303 @@ describe('ModifiedExpenseMessage', () => {
             });
         });
     });
+
+    describe('getForReportActionTemp', () => {
+        // getForReportActionTemp is a temporary function that takes translate, policy, policyTags, and currentUserLogin as parameters
+        // instead of using module-level Onyx connections. This allows React components to properly re-render when the underlying data changes.
+        // These tests mirror the getForReportAction tests to ensure the same behavior.
+
+        describe('when the amount is changed', () => {
+            const reportAction = {
+                ...createRandomReportAction(1),
+                actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
+                originalMessage: {
+                    amount: 1800,
+                    currency: CONST.CURRENCY.USD,
+                    oldAmount: 1255,
+                    oldCurrency: CONST.CURRENCY.USD,
+                },
+            };
+
+            it('returns the correct text message', () => {
+                const expectedResult = `changed the amount to $18.00 (previously $12.55)`;
+
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+
+                expect(result).toEqual(expectedResult);
+            });
+        });
+
+        describe('when the amount is changed and the description is removed', () => {
+            const reportAction = {
+                ...createRandomReportAction(1),
+                actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
+                originalMessage: {
+                    amount: 1800,
+                    currency: CONST.CURRENCY.USD,
+                    oldAmount: 1255,
+                    oldCurrency: CONST.CURRENCY.USD,
+                    newComment: '',
+                    oldComment: 'this is for the shuttle',
+                },
+            };
+
+            it('returns the correct text message', () => {
+                const expectedResult = 'changed the amount to $18.00 (previously $12.55)\nremoved the description (previously "this is for the shuttle")';
+
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+
+                expect(result).toEqual(expectedResult);
+            });
+        });
+
+        describe('when the merchant is set', () => {
+            const reportAction = {
+                ...createRandomReportAction(1),
+                actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
+                originalMessage: {
+                    oldMerchant: '',
+                    merchant: 'Big Belly',
+                },
+            };
+
+            it('returns the correct text message', () => {
+                const expectedResult = `set the merchant to "Big Belly"`;
+
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+
+                expect(result).toEqual(expectedResult);
+            });
+        });
+
+        describe('when the merchant is removed', () => {
+            const reportAction = {
+                ...createRandomReportAction(1),
+                actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
+                originalMessage: {
+                    merchant: '',
+                    oldMerchant: 'Big Belly',
+                },
+            };
+
+            it('returns the correct text message', () => {
+                const expectedResult = `removed the merchant (previously "Big Belly")`;
+
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+
+                expect(result).toEqual(expectedResult);
+            });
+        });
+
+        describe('when the category is changed', () => {
+            const reportAction = {
+                ...createRandomReportAction(1),
+                actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
+                originalMessage: {
+                    category: 'Travel',
+                    oldCategory: 'Food',
+                } as OriginalMessageModifiedExpense,
+            };
+
+            it('returns the correct text message', () => {
+                const expectedResult = `changed the category to "Travel" (previously "Food")`;
+
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+
+                expect(result).toEqual(expectedResult);
+            });
+        });
+
+        describe('when the category is changed with AI attribution', () => {
+            const reportAction = {
+                ...createRandomReportAction(1),
+                actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
+                originalMessage: {
+                    category: 'Travel',
+                    oldCategory: 'Food',
+                    source: CONST.CATEGORY_SOURCE.AI,
+                } as OriginalMessageModifiedExpense,
+            };
+
+            it('returns the correct text message with AI attribution', () => {
+                const expectedResult = `changed the category based on past activity to "Travel" (previously "Food")`;
+
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+
+                expect(result).toEqual(expectedResult);
+            });
+        });
+
+        describe('when the category is changed with MCC attribution', () => {
+            const reportAction = {
+                ...createRandomReportAction(1),
+                actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
+                originalMessage: {
+                    category: 'Travel',
+                    oldCategory: 'Food',
+                    source: CONST.CATEGORY_SOURCE.MCC,
+                } as OriginalMessageModifiedExpense,
+            };
+
+            it('returns the correct text message with MCC attribution for non-admin', () => {
+                const expectedResult = `changed the category based on workspace rule to "Travel" (previously "Food")`;
+
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+
+                expect(result).toEqual(expectedResult);
+            });
+
+            it('returns the correct workspace rules link for admin', () => {
+                const mockPolicy: Policy = {
+                    id: 'AbC123XyZ789',
+                    name: 'Test Policy',
+                    role: CONST.POLICY.ROLE.ADMIN,
+                    type: CONST.POLICY.TYPE.TEAM,
+                    owner: 'test@example.com',
+                    outputCurrency: 'USD',
+                    isPolicyExpenseChatEnabled: true,
+                };
+
+                jest.spyOn(PolicyUtils, 'isPolicyAdmin').mockReturnValue(true);
+
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    policy: mockPolicy,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+
+                // Verify the policyID in the URL exactly matches the policy.id (case-preserved)
+                expect(result).toContain(`workspaces/${mockPolicy.id}/rules`);
+                expect(result).toContain('href=');
+                expect(result).toContain('workspace rules</a>');
+            });
+        });
+
+        describe('when the distance is changed', () => {
+            const reportAction = {
+                ...createRandomReportAction(1),
+                actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
+                originalMessage: {
+                    oldMerchant: '1.00 mi @ $0.70 / mi',
+                    merchant: '10.00 mi @ $0.70 / mi',
+                    oldAmount: 70,
+                    amount: 700,
+                    oldCurrency: CONST.CURRENCY.USD,
+                    currency: CONST.CURRENCY.USD,
+                },
+            };
+
+            it('then the message says the distance is changed and shows the new and old merchant and amount', () => {
+                const expectedResult = `changed the distance to ${reportAction.originalMessage.merchant} (previously ${reportAction.originalMessage.oldMerchant}), which updated the amount to $7.00 (previously $0.70)`;
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+                expect(result).toEqual(expectedResult);
+            });
+        });
+
+        describe('when moving an expense', () => {
+            it('returns the movedFromOrToReportMessage message when provided', () => {
+                const reportAction = {
+                    ...createRandomReportAction(1),
+                    actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
+                };
+
+                const expectedResult = 'moved an expense';
+
+                const movedFromReport = {
+                    ...createRandomReport(1, undefined),
+                    reportName: '',
+                };
+
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    movedFromReport,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+                expect(result).toEqual(expectedResult);
+            });
+        });
+
+        describe('when the report action is not a modified expense action', () => {
+            const reportAction = {
+                ...createRandomReportAction(1),
+                actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
+            };
+
+            it('returns an empty string', () => {
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+
+                expect(result).toEqual('');
+            });
+        });
+
+        describe('when there are no changes in the original message', () => {
+            const reportAction = {
+                ...createRandomReportAction(1),
+                actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
+                originalMessage: {},
+            };
+
+            it('returns the generic changed expense message', () => {
+                const expectedResult = 'changed the expense';
+
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+
+                expect(result).toEqual(expectedResult);
+            });
+        });
+    });
 });

From a2dceaff0d6ef6979860cf130d83587f5b79cbab Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= <marco@expensify.com>
Date: Thu, 5 Feb 2026 16:28:56 -0600
Subject: [PATCH 11/16] Address PR review feedback: simplify eslint comments,
 revert param rename, and revert submodule

- Simplify eslint-disable comments to reference issue #66336 instead of verbose explanations
- Revert currentUserLogin parameter rename (keep original name instead of currentUserLoginParam)
- Revert unintended Mobile-Expensify submodule change
---
 Mobile-Expensify                   | 2 +-
 src/libs/ModifiedExpenseMessage.ts | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/Mobile-Expensify b/Mobile-Expensify
index 574c396c1731..b9694c7c450b 160000
--- a/Mobile-Expensify
+++ b/Mobile-Expensify
@@ -1 +1 @@
-Subproject commit 574c396c1731467091f7865a3e460343d25e2397
+Subproject commit b9694c7c450b09aa59690792b178d36bcab911f7
diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index 90aa5e4dc8a2..90783aa2ab24 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -28,7 +28,7 @@ import {getPolicyName, getReportName, getRootParentReport, isPolicyExpenseChat,
 import {getFormattedAttendees, getTagArrayFromName} from './TransactionUtils';
 
 let allPolicyTags: OnyxCollection<PolicyTagLists> = {};
-// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage.getForReportAction is called by utility files (ReportUtils.getModifiedExpenseMessage, OptionsListUtils, ReportNameUtils) that cannot use React hooks. The global policyTags state is required for these non-React contexts. Migration to useOnyx will happen when these utility files are converted to custom hooks.
+// eslint-disable-next-line @typescript-eslint/no-deprecated -- Onyx.connectWithoutView is being removed in https://github.com/Expensify/App/issues/66336
 Onyx.connectWithoutView({
     key: ONYXKEYS.COLLECTION.POLICY_TAGS,
     waitForCollectionCallback: true,
@@ -45,7 +45,7 @@ let environmentURL: string;
 getEnvironmentURL().then((url: string) => (environmentURL = url));
 
 let currentUserLogin = '';
-// eslint-disable-next-line @typescript-eslint/no-deprecated -- ModifiedExpenseMessage.getForReportAction requires currentUserLogin for isPolicyAdmin checks. This function is called by non-React utility files (ReportUtils.getModifiedExpenseMessage, OptionsListUtils, ReportNameUtils) that cannot use React hooks. Migration to useOnyx will happen when these utility files are converted to custom hooks.
+// eslint-disable-next-line @typescript-eslint/no-deprecated -- Onyx.connectWithoutView is being removed in https://github.com/Expensify/App/issues/66336
 Onyx.connectWithoutView({
     key: ONYXKEYS.SESSION,
     callback: (value) => {
@@ -516,7 +516,7 @@ function getForReportActionTemp({
     movedFromReport,
     movedToReport,
     policyTags,
-    currentUserLogin: currentUserLoginParam,
+    currentUserLogin,
 }: {
     translate: LocalizedTranslate;
     reportAction: OnyxEntry<ReportAction>;
@@ -614,7 +614,7 @@ function getForReportActionTemp({
         if (reportActionOriginalMessage?.source === CONST.CATEGORY_SOURCE.AI) {
             categoryLabel += ` ${translate('iou.basedOnAI')}`;
         } else if (reportActionOriginalMessage?.source === CONST.CATEGORY_SOURCE.MCC) {
-            const isAdmin = isPolicyAdmin(policy, currentUserLoginParam);
+            const isAdmin = isPolicyAdmin(policy, currentUserLogin);
 
             // For admins, create a hyperlink to the workspace rules page
             if (isAdmin && policy?.id) {

From 0bd9a8daede03942973ddeb1e302c6a604b9aebc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= <marco@expensify.com>
Date: Thu, 5 Feb 2026 20:07:00 -0600
Subject: [PATCH 12/16] fix: rename module-level currentUserLogin to
 storedCurrentUserLogin to avoid no-shadow lint error

Co-authored-by: Cursor <cursoragent@cursor.com>
---
 src/libs/ModifiedExpenseMessage.ts | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index 42cc5e17f2d9..db4054336fb5 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -44,7 +44,7 @@ Onyx.connectWithoutView({
 let environmentURL: string;
 getEnvironmentURL().then((url: string) => (environmentURL = url));
 
-let currentUserLogin = '';
+let storedCurrentUserLogin = '';
 // eslint-disable-next-line @typescript-eslint/no-deprecated -- Onyx.connectWithoutView is being removed in https://github.com/Expensify/App/issues/66336
 Onyx.connectWithoutView({
     key: ONYXKEYS.SESSION,
@@ -53,7 +53,7 @@ Onyx.connectWithoutView({
         if (!value) {
             return;
         }
-        currentUserLogin = value?.email ?? '';
+        storedCurrentUserLogin = value?.email ?? '';
     },
 });
 
@@ -166,7 +166,7 @@ function getForExpenseMovedFromSelfDM(translate: LocalizedTranslate, destination
     // In NewDot, the "Move report" flow only supports moving expenses from self-DM to:
     // - A policy expense chat
     // - A 1:1 DM
-    const currentUserAccountID = getPersonalDetailByEmail(currentUserLogin)?.accountID;
+    const currentUserAccountID = getPersonalDetailByEmail(storedCurrentUserLogin)?.accountID;
     const reportName = isPolicyExpenseChat(rootParentReport)
         ? getPolicyExpenseChatName({report: rootParentReport})
         : buildReportNameFromParticipantNames({report: rootParentReport, currentUserAccountID});
@@ -332,7 +332,7 @@ function getForReportAction({
         } else if (reportActionOriginalMessage?.source === CONST.CATEGORY_SOURCE.MCC) {
             // eslint-disable-next-line @typescript-eslint/no-deprecated
             const policy = getPolicy(policyID);
-            const isAdmin = isPolicyAdmin(policy, currentUserLogin);
+            const isAdmin = isPolicyAdmin(policy, storedCurrentUserLogin);
 
             // For admins, create a hyperlink to the workspace rules page
             if (isAdmin && policy?.id) {

From cbec8b1a3fbee796f0f6423184a79c759b929e93 Mon Sep 17 00:00:00 2001
From: MelvinBot <infra+melvinbot@expensify.com>
Date: Tue, 10 Feb 2026 17:55:38 +0000
Subject: [PATCH 13/16] Fix billable/reimbursable localization and add
 AI-generated fallback in getForReportActionTemp
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Translate billable/reimbursable values before passing to buildMessageFragmentForValue
to match getForReportAction behavior. Add missing AI-generated fallback when message
is empty. Add test cases for both fixes.

Co-authored-by: Marco Chávez <marcochavezf@users.noreply.github.com>
---
 src/libs/ModifiedExpenseMessage.ts       | 21 ++++++--
 tests/unit/ModifiedExpenseMessageTest.ts | 69 ++++++++++++++++++++++++
 2 files changed, 86 insertions(+), 4 deletions(-)

diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index db4054336fb5..792f44a3bc72 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -697,10 +697,12 @@ function getForReportActionTemp({
 
     const hasModifiedBillable = isReportActionOriginalMessageAnObject && 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage;
     if (hasModifiedBillable) {
+        const oldBillable = reportActionOriginalMessage?.oldBillable === 'billable' ? translate('common.billable').toLowerCase() : translate('common.nonBillable').toLowerCase();
+        const newBillable = reportActionOriginalMessage?.billable === 'billable' ? translate('common.billable').toLowerCase() : translate('common.nonBillable').toLowerCase();
         buildMessageFragmentForValue(
             translate,
-            reportActionOriginalMessage?.billable ?? '',
-            reportActionOriginalMessage?.oldBillable ?? '',
+            newBillable,
+            oldBillable,
             translate('iou.expense'),
             true,
             setFragments,
@@ -711,10 +713,14 @@ function getForReportActionTemp({
 
     const hasModifiedReimbursable = isReportActionOriginalMessageAnObject && 'oldReimbursable' in reportActionOriginalMessage && 'reimbursable' in reportActionOriginalMessage;
     if (hasModifiedReimbursable) {
+        const oldReimbursable =
+            reportActionOriginalMessage?.oldReimbursable === 'reimbursable' ? translate('iou.reimbursable').toLowerCase() : translate('iou.nonReimbursable').toLowerCase();
+        const newReimbursable =
+            reportActionOriginalMessage?.reimbursable === 'reimbursable' ? translate('iou.reimbursable').toLowerCase() : translate('iou.nonReimbursable').toLowerCase();
         buildMessageFragmentForValue(
             translate,
-            reportActionOriginalMessage?.reimbursable ?? '',
-            reportActionOriginalMessage?.oldReimbursable ?? '',
+            newReimbursable,
+            oldReimbursable,
             translate('iou.expense'),
             true,
             setFragments,
@@ -745,6 +751,13 @@ function getForReportActionTemp({
         getMessageLine(translate, `\n${translate('iou.removed')}`, removalFragments);
 
     if (message === '') {
+        // If we don't have enough structured information to build a detailed message but we
+        // know the change was AI-generated, fall back to an AI-attributed generic summary so
+        // users can still understand that Concierge updated the expense automatically.
+        if (reportActionOriginalMessage?.aiGenerated) {
+            return `${translate('iou.changedTheExpense')} ${translate('iou.basedOnAI')}`;
+        }
+
         return translate('iou.changedTheExpense');
     }
     return `${message.substring(1, message.length)}`;
diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts
index 0ed4b258ae19..0f7dcf177869 100644
--- a/tests/unit/ModifiedExpenseMessageTest.ts
+++ b/tests/unit/ModifiedExpenseMessageTest.ts
@@ -1250,5 +1250,74 @@ describe('ModifiedExpenseMessage', () => {
                 expect(result).toEqual(expectedResult);
             });
         });
+
+        describe('when the billable field is changed', () => {
+            const reportAction = {
+                ...createRandomReportAction(1),
+                actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
+                originalMessage: {
+                    oldBillable: 'nonBillable',
+                    billable: 'billable',
+                },
+            };
+
+            it('returns the correct translated text message', () => {
+                const expectedResult = 'changed the expense to "billable" (previously "non-billable")';
+
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+
+                expect(result).toEqual(expectedResult);
+            });
+        });
+
+        describe('when the reimbursable field is changed', () => {
+            const reportAction = {
+                ...createRandomReportAction(1),
+                actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
+                originalMessage: {
+                    oldReimbursable: 'reimbursable',
+                    reimbursable: 'nonReimbursable',
+                },
+            };
+
+            it('returns the correct translated text message', () => {
+                const expectedResult = 'changed the expense to "non-reimbursable" (previously "reimbursable")';
+
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+
+                expect(result).toEqual(expectedResult);
+            });
+        });
+
+        describe('when there are no changes but aiGenerated is true', () => {
+            const reportAction = {
+                ...createRandomReportAction(1),
+                actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE,
+                originalMessage: {aiGenerated: true},
+            };
+
+            it('returns the AI-attributed message', () => {
+                const expectedResult = 'changed the expense based on past activity';
+
+                const result = getForReportActionTemp({
+                    translate: translateLocal,
+                    reportAction,
+                    policyTags: undefined,
+                    currentUserLogin: 'test@example.com',
+                });
+
+                expect(result).toEqual(expectedResult);
+            });
+        });
     });
 });

From 0515fdaaf762f2e3254f6b602b3c3a4d19cac9a3 Mon Sep 17 00:00:00 2001
From: MelvinBot <infra+melvinbot@expensify.com>
Date: Tue, 10 Feb 2026 18:38:37 +0000
Subject: [PATCH 14/16] Fix Prettier formatting in ModifiedExpenseMessage.ts
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: Marco Chávez <marcochavezf@users.noreply.github.com>
---
 src/libs/ModifiedExpenseMessage.ts | 25 +++----------------------
 1 file changed, 3 insertions(+), 22 deletions(-)

diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts
index 792f44a3bc72..3c4775b6d8cb 100644
--- a/src/libs/ModifiedExpenseMessage.ts
+++ b/src/libs/ModifiedExpenseMessage.ts
@@ -699,34 +699,15 @@ function getForReportActionTemp({
     if (hasModifiedBillable) {
         const oldBillable = reportActionOriginalMessage?.oldBillable === 'billable' ? translate('common.billable').toLowerCase() : translate('common.nonBillable').toLowerCase();
         const newBillable = reportActionOriginalMessage?.billable === 'billable' ? translate('common.billable').toLowerCase() : translate('common.nonBillable').toLowerCase();
-        buildMessageFragmentForValue(
-            translate,
-            newBillable,
-            oldBillable,
-            translate('iou.expense'),
-            true,
-            setFragments,
-            removalFragments,
-            changeFragments,
-        );
+        buildMessageFragmentForValue(translate, newBillable, oldBillable, translate('iou.expense'), true, setFragments, removalFragments, changeFragments);
     }
 
     const hasModifiedReimbursable = isReportActionOriginalMessageAnObject && 'oldReimbursable' in reportActionOriginalMessage && 'reimbursable' in reportActionOriginalMessage;
     if (hasModifiedReimbursable) {
         const oldReimbursable =
             reportActionOriginalMessage?.oldReimbursable === 'reimbursable' ? translate('iou.reimbursable').toLowerCase() : translate('iou.nonReimbursable').toLowerCase();
-        const newReimbursable =
-            reportActionOriginalMessage?.reimbursable === 'reimbursable' ? translate('iou.reimbursable').toLowerCase() : translate('iou.nonReimbursable').toLowerCase();
-        buildMessageFragmentForValue(
-            translate,
-            newReimbursable,
-            oldReimbursable,
-            translate('iou.expense'),
-            true,
-            setFragments,
-            removalFragments,
-            changeFragments,
-        );
+        const newReimbursable = reportActionOriginalMessage?.reimbursable === 'reimbursable' ? translate('iou.reimbursable').toLowerCase() : translate('iou.nonReimbursable').toLowerCase();
+        buildMessageFragmentForValue(translate, newReimbursable, oldReimbursable, translate('iou.expense'), true, setFragments, removalFragments, changeFragments);
     }
 
     const hasModifiedAttendees = isReportActionOriginalMessageAnObject && 'oldAttendees' in reportActionOriginalMessage && 'newAttendees' in reportActionOriginalMessage;

From 7d18ddbb6e1e0773a4438e06d3e78a58d4db60c3 Mon Sep 17 00:00:00 2001
From: MelvinBot <infra+melvinbot@expensify.com>
Date: Tue, 10 Feb 2026 22:49:18 +0000
Subject: [PATCH 15/16] Improve policyID comment and add DEFAULT_TAG_LIST
 fallback for policyTags
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Update the comment on policyIDForTags to explain why a fake policyID requires
falling back to policyForMovingExpensesID. Add CONST.POLICY.DEFAULT_TAG_LIST
fallback when policyTags is undefined to prevent tag-only edits from degrading
to the generic "changed the expense" message.

Co-authored-by: Marco Chávez <marcochavezf@users.noreply.github.com>
---
 src/pages/inbox/report/ReportActionItem.tsx | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx
index 67bff38f8ab5..3294c777d923 100644
--- a/src/pages/inbox/report/ReportActionItem.tsx
+++ b/src/pages/inbox/report/ReportActionItem.tsx
@@ -101,7 +101,10 @@ function ReportActionItem({
     const isOriginalReportArchived = useReportIsArchived(originalReportID);
     const {accountID: currentUserAccountID, login: currentUserLogin} = useCurrentUserPersonalDetails();
     const {policyForMovingExpensesID} = usePolicyForMovingExpenses();
-    // Use policyForMovingExpensesID when policyID is fake to ensure tag edits on moved expenses use the correct workspace's tag lists
+    // When an expense is moved from a self-DM to a workspace, the report's policyID is temporarily
+    // set to a fake placeholder (CONST.POLICY.OWNER_EMAIL_FAKE). Looking up POLICY_TAGS with that
+    // fake ID would return nothing, so we fall back to policyForMovingExpensesID (the actual
+    // destination workspace) to fetch the correct tag list for display.
     const policyIDForTags = report?.policyID === CONST.POLICY.OWNER_EMAIL_FAKE && policyForMovingExpensesID ? policyForMovingExpensesID : report?.policyID;
     const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyIDForTags}`, {canBeMissing: true});
     const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
@@ -171,7 +174,7 @@ function ReportActionItem({
                 policy,
                 movedFromReport,
                 movedToReport,
-                policyTags,
+                policyTags: policyTags ?? CONST.POLICY.DEFAULT_TAG_LIST,
                 currentUserLogin: currentUserLogin ?? '',
             })}
             getTransactionsWithReceipts={getTransactionsWithReceipts}

From 30b188a76e5af62b59be6617b61cbbde3fd21408 Mon Sep 17 00:00:00 2001
From: MelvinBot <infra+melvinbot@expensify.com>
Date: Wed, 11 Feb 2026 02:50:42 +0000
Subject: [PATCH 16/16] Fix: use email instead of login from
 useCurrentUserPersonalDetails
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

CurrentUserPersonalDetailsProvider backfills email from session but not
login, so login can be undefined during loading. Use email to maintain
parity with the old session-backed value.

Co-authored-by: Marco Chávez <marcochavezf@users.noreply.github.com>
---
 src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx | 2 +-
 src/pages/inbox/report/ReportActionItem.tsx               | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
index b63bab9296f2..a4bd5dd86469 100644
--- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx
@@ -770,7 +770,7 @@ const ContextMenuActions: ContextMenuAction[] = [
                         movedFromReport,
                         movedToReport,
                         policyTags,
-                        currentUserLogin: currentUserPersonalDetails?.login ?? '',
+                        currentUserLogin: currentUserPersonalDetails?.email ?? '',
                     });
                     Clipboard.setString(modifyExpenseMessage);
                 } else if (isReimbursementDeQueuedOrCanceledAction(reportAction)) {
diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx
index 3294c777d923..eb03284bf137 100644
--- a/src/pages/inbox/report/ReportActionItem.tsx
+++ b/src/pages/inbox/report/ReportActionItem.tsx
@@ -99,7 +99,7 @@ function ReportActionItem({
     const originalReportID = useOriginalReportID(reportID, action);
     const originalReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`];
     const isOriginalReportArchived = useReportIsArchived(originalReportID);
-    const {accountID: currentUserAccountID, login: currentUserLogin} = useCurrentUserPersonalDetails();
+    const {accountID: currentUserAccountID, email: currentUserEmail} = useCurrentUserPersonalDetails();
     const {policyForMovingExpensesID} = usePolicyForMovingExpenses();
     // When an expense is moved from a self-DM to a workspace, the report's policyID is temporarily
     // set to a fake placeholder (CONST.POLICY.OWNER_EMAIL_FAKE). Looking up POLICY_TAGS with that
@@ -175,7 +175,7 @@ function ReportActionItem({
                 movedFromReport,
                 movedToReport,
                 policyTags: policyTags ?? CONST.POLICY.DEFAULT_TAG_LIST,
-                currentUserLogin: currentUserLogin ?? '',
+                currentUserLogin: currentUserEmail ?? '',
             })}
             getTransactionsWithReceipts={getTransactionsWithReceipts}
             clearError={clearError}
