From 20456755ade58e148dce9849c217fc82f67160f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Tue, 18 Nov 2025 12:27:55 -0600 Subject: [PATCH 01/12] Update translations to include dynamic links for workspace rules in multiple languages --- src/languages/de.ts | 2 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/languages/fr.ts | 2 +- src/languages/it.ts | 2 +- src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 2 +- src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 6ee628ce4b415..f4db04e474379 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1335,7 +1335,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `änderte das ${translatedChangedField} zu ${newMerchant} (zuvor ${oldMerchant}), wodurch der Betrag auf ${newAmountToDisplay} aktualisiert wurde (zuvor ${oldAmountToDisplay})`, basedOnAI: 'basierend auf früheren Aktivitäten', - basedOnMCC: 'basierend auf Arbeitsbereichsregel', + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `basierend auf Arbeitsbereichsregeln` : 'basierend auf Arbeitsbereichsregel'), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `für ${comment}` : 'Ausgabe'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `Rechnungsbericht Nr. ${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} gesendet${comment ? `für ${comment}` : ''}`, diff --git a/src/languages/en.ts b/src/languages/en.ts index 86ffa35a4329e..483f8d0293616 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1317,7 +1317,7 @@ const translations = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `changed the ${translatedChangedField} to ${newMerchant} (previously ${oldMerchant}), which updated the amount to ${newAmountToDisplay} (previously ${oldAmountToDisplay})`, basedOnAI: 'based on past activity', - basedOnMCC: 'based on workspace rule', + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `based on workspace rules` : 'based on workspace rule'), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `for ${comment}` : 'expense'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `Invoice Report #${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index b96ee87a3fce5..f4ac691005184 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -980,7 +980,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}) => `cambió la ${translatedChangedField} a ${newMerchant} (previamente ${oldMerchant}), lo que cambió el importe a ${newAmountToDisplay} (previamente ${oldAmountToDisplay})`, basedOnAI: 'basado en actividad pasada', - basedOnMCC: 'basado en regla del espacio de trabajo', + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `basado en reglas del espacio de trabajo` : 'basado en regla del espacio de trabajo'), threadExpenseReportName: ({formattedAmount, comment}) => `${comment ? `${formattedAmount} para ${comment}` : `Gasto de ${formattedAmount}`}`, invoiceReportName: ({linkedReportID}) => `Informe de facturación #${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 4f5ee5d17deea..4669ee9220aea 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1339,7 +1339,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `a changé le ${translatedChangedField} en ${newMerchant} (précédemment ${oldMerchant}), ce qui a mis à jour le montant à ${newAmountToDisplay} (précédemment ${oldAmountToDisplay})`, basedOnAI: "basé sur l'activité passée", - basedOnMCC: "basé sur la règle de l'espace de travail", + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `basé sur les règles de l'espace de travail` : "basé sur la règle de l'espace de travail"), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `pour ${comment}` : 'dépense'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `Rapport de Facture n°${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} envoyé${comment ? `pour ${comment}` : ''}`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 0e54f3daae138..5b9f03687a6cd 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1333,7 +1333,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `ha cambiato il ${translatedChangedField} in ${newMerchant} (precedentemente ${oldMerchant}), il che ha aggiornato l'importo a ${newAmountToDisplay} (precedentemente ${oldAmountToDisplay})`, basedOnAI: 'basato su attività passate', - basedOnMCC: 'basato su regola dello spazio di lavoro', + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `basato su regole dello spazio di lavoro` : 'basato su regola dello spazio di lavoro'), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `per ${comment}` : 'spesa'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `Rapporto Fattura n. ${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} inviato${comment ? `per ${comment}` : ''}`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 9efb79b3a78da..2aa9799ea5ec9 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1335,7 +1335,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `${translatedChangedField}を${newMerchant}に変更しました(以前は${oldMerchant})、これにより金額が${newAmountToDisplay}に更新されました(以前は${oldAmountToDisplay})。`, basedOnAI: '過去のアクティビティに基づく', - basedOnMCC: 'ワークスペースルールに基づく', + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `ワークスペースルールに基づく` : 'ワークスペースルールに基づく'), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `${comment}用` : '経費'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `請求書レポート #${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} 送信済み${comment ? `${comment} のために` : ''}`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index edf0a885378d5..53ccab58d14f1 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1333,7 +1333,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `veranderde de ${translatedChangedField} naar ${newMerchant} (voorheen ${oldMerchant}), wat het bedrag bijwerkte naar ${newAmountToDisplay} (voorheen ${oldAmountToDisplay})`, basedOnAI: 'op basis van eerdere activiteit', - basedOnMCC: 'op basis van werkruimteregel', + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `op basis van werkruimteregels` : 'op basis van werkruimteregel'), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `voor ${comment}` : 'uitgave'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `Factuurrapport #${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} verzonden${comment ? `voor ${comment}` : ''}`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index d88381f33548f..0a391c9520153 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1332,7 +1332,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `zmienił ${translatedChangedField} na ${newMerchant} (wcześniej ${oldMerchant}), co zaktualizowało kwotę na ${newAmountToDisplay} (wcześniej ${oldAmountToDisplay})`, basedOnAI: 'na podstawie wcześniejszej aktywności', - basedOnMCC: 'na podstawie reguły przestrzeni roboczej', + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `na podstawie reguł przestrzeni roboczej` : 'na podstawie reguły przestrzeni roboczej'), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `dla ${comment}` : 'wydatek'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `Raport faktury nr ${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} wysłano${comment ? `dla ${comment}` : ''}`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 39cf4d45f7af5..885301562511f 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1331,7 +1331,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `alterou o ${translatedChangedField} para ${newMerchant} (anteriormente ${oldMerchant}), o que atualizou o valor para ${newAmountToDisplay} (anteriormente ${oldAmountToDisplay})`, basedOnAI: 'com base em atividades passadas', - basedOnMCC: 'com base na regra do espaço de trabalho', + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `com base nas regras do espaço de trabalho` : 'com base na regra do espaço de trabalho'), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `para ${comment}` : 'despesa'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `Relatório de Fatura nº ${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? `para ${comment}` : ''}`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 7762508a518e0..50da26db4df44 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1313,7 +1313,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `将${translatedChangedField}更改为${newMerchant}(之前为${oldMerchant}),这更新了金额为${newAmountToDisplay}(之前为${oldAmountToDisplay})`, basedOnAI: '基于过去的活动', - basedOnMCC: '基于工作空间规则', + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `基于工作空间规则` : '基于工作空间规则'), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `为${comment}` : '费用'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `发票报告 #${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} 已发送${comment ? `对于${comment}` : ''}`, From cd95612cbfac0121cf78caa2f1bb84f3ba2f5c8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Tue, 18 Nov 2025 12:42:34 -0600 Subject: [PATCH 02/12] Enhance modified expense messages with dynamic links for workspace rules based on user permissions --- src/libs/ModifiedExpenseMessage.ts | 51 +++++++++++++++++-- .../report/ContextMenu/ContextMenuActions.tsx | 1 + .../home/report/PureReportActionItem.tsx | 6 ++- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index ae15f2bcc7854..24873e4df45cc 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -4,15 +4,17 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type {PolicyTagLists, Report, ReportAction} from '@src/types/onyx'; import {getDecodedCategoryName} from './CategoryUtils'; import {convertToDisplayString} from './CurrencyUtils'; import DateUtils from './DateUtils'; +import {getEnvironmentURL} from './Environment/Environment'; // eslint-disable-next-line @typescript-eslint/no-deprecated import {translateLocal} from './Localize'; import Log from './Log'; import Parser from './Parser'; -import {getCleanedTagName, getSortedTagKeys} from './PolicyUtils'; +import {getCleanedTagName, getPolicy, getSortedTagKeys, isPolicyAdmin} from './PolicyUtils'; import {getOriginalMessage, isModifiedExpenseAction} from './ReportActionsUtils'; // eslint-disable-next-line import/no-cycle import {buildReportNameFromParticipantNames, getPolicyExpenseChatName, getPolicyName, getReportName, getRootParentReport, isPolicyExpenseChat, isSelfDM} from './ReportUtils'; @@ -31,6 +33,21 @@ Onyx.connect({ }, }); +let environmentURL: string; +getEnvironmentURL().then((url: string) => (environmentURL = url)); + +let currentUserLogin = ''; +Onyx.connect({ + key: ONYXKEYS.SESSION, + callback: (value) => { + // When signed out, value is undefined + if (!value) { + return; + } + currentUserLogin = value?.email ?? ''; + }, +}); + /** * Builds the partial message fragment for a modified field on the expense. */ @@ -283,7 +300,18 @@ function getForReportAction({ categoryLabel += ` ${translateLocal('iou.basedOnAI')}`; } else if (reportActionOriginalMessage?.source === CONST.CATEGORY_SOURCE.MCC) { // eslint-disable-next-line @typescript-eslint/no-deprecated - categoryLabel += ` ${translateLocal('iou.basedOnMCC')}`; + const policy = getPolicy(policyID); + const isAdmin = isPolicyAdmin(policy, currentUserLogin); + + // For admins, create a hyperlink to the workspace rules page + if (isAdmin && policy?.id) { + const rulesLink = `${environmentURL}/${ROUTES.WORKSPACE_RULES.getRoute(policy.id)}`; + // eslint-disable-next-line @typescript-eslint/no-deprecated + categoryLabel += ` ${translateLocal('iou.basedOnMCC', {rulesLink})}`; + } else { + // eslint-disable-next-line @typescript-eslint/no-deprecated + categoryLabel += ` ${translateLocal('iou.basedOnMCC', {rulesLink: ''})}`; + } } buildMessageFragmentForValue( @@ -294,6 +322,8 @@ function getForReportAction({ setFragments, removalFragments, changeFragments, + // Don't convert to lowercase when we have source attribution (to preserve any HTML links) + !reportActionOriginalMessage?.source, ); } @@ -410,11 +440,13 @@ function getForReportAction({ */ function getForReportActionTemp({ reportAction, + policyID, movedFromReport, movedToReport, policyTags, }: { reportAction: OnyxEntry; + policyID?: string | undefined; movedFromReport?: OnyxEntry; movedToReport?: OnyxEntry; policyTags: OnyxEntry; @@ -505,7 +537,18 @@ function getForReportActionTemp({ categoryLabel += ` ${translateLocal('iou.basedOnAI')}`; } else if (reportActionOriginalMessage?.source === CONST.CATEGORY_SOURCE.MCC) { // eslint-disable-next-line @typescript-eslint/no-deprecated - categoryLabel += ` ${translateLocal('iou.basedOnMCC')}`; + const policy = getPolicy(policyID); + const isAdmin = isPolicyAdmin(policy, currentUserLogin); + + // For admins, create a hyperlink to the workspace rules page + if (isAdmin && policy?.id) { + const rulesLink = `${environmentURL}/${ROUTES.WORKSPACE_RULES.getRoute(policy.id)}`; + // eslint-disable-next-line @typescript-eslint/no-deprecated + categoryLabel += ` ${translateLocal('iou.basedOnMCC', {rulesLink})}`; + } else { + // eslint-disable-next-line @typescript-eslint/no-deprecated + categoryLabel += ` ${translateLocal('iou.basedOnMCC', {rulesLink: ''})}`; + } } buildMessageFragmentForValue( @@ -516,6 +559,8 @@ function getForReportActionTemp({ setFragments, removalFragments, changeFragments, + // Don't convert to lowercase when we have source attribution (to preserve any HTML links) + !reportActionOriginalMessage?.source, ); } diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 678a3b1710c25..3179434617fac 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -545,6 +545,7 @@ const ContextMenuActions: ContextMenuAction[] = [ } else if (isModifiedExpenseAction(reportAction)) { const modifyExpenseMessage = getForReportActionTemp({ reportAction, + policyID: report?.policyID, movedFromReport, movedToReport, policyTags, diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 277af96309207..d006e391bc35f 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -1135,7 +1135,11 @@ function PureReportActionItem({ } else if (isReimbursementDeQueuedOrCanceledAction(action)) { children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE) { - children = ; + children = ( + + ${modifiedExpenseMessage}`} /> + + ); } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.SUBMITTED) || isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.SUBMITTED_AND_CLOSED) || isMarkAsClosedAction(action)) { const wasSubmittedViaHarvesting = !isMarkAsClosedAction(action) ? (getOriginalMessage(action)?.harvesting ?? false) : false; if (wasSubmittedViaHarvesting) { From 9a65aaab23da6be9020e4f8021bf13dd73e69340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Tue, 18 Nov 2025 12:42:39 -0600 Subject: [PATCH 03/12] Capitalize "Category" in modified expense messages for consistency with attribution sources --- tests/unit/ModifiedExpenseMessageTest.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index 24fd4c248cb7f..0bb2f3e234b7f 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -620,7 +620,8 @@ describe('ModifiedExpenseMessage', () => { }; it('returns the correct text message with AI attribution', () => { - const expectedResult = `changed the category based on past activity to "Travel" (previously "Food")`; + // Note: Category is capitalized when source attribution is present + const expectedResult = `changed the Category based on past activity to "Travel" (previously "Food")`; const result = getForReportAction({reportAction, policyID: report.policyID}); @@ -640,7 +641,8 @@ describe('ModifiedExpenseMessage', () => { }; it('returns the correct text message with MCC attribution', () => { - const expectedResult = `changed the category based on workspace rule to "Travel" (previously "Food")`; + // Note: Category is capitalized when MCC attribution is used (to preserve HTML link casing) + const expectedResult = `changed the Category based on workspace rule to "Travel" (previously "Food")`; const result = getForReportAction({reportAction, policyID: report.policyID}); @@ -660,7 +662,8 @@ describe('ModifiedExpenseMessage', () => { }; it('returns the correct text message with AI attribution', () => { - const expectedResult = `set the category based on past activity to "Travel"`; + // Note: Category is capitalized when source attribution is present + const expectedResult = `set the Category based on past activity to "Travel"`; const result = getForReportAction({reportAction, policyID: report.policyID}); @@ -680,7 +683,8 @@ describe('ModifiedExpenseMessage', () => { }; it('returns the correct text message with AI attribution', () => { - const expectedResult = `removed the category based on past activity (previously "Travel")`; + // Note: Category is capitalized when source attribution is present + const expectedResult = `removed the Category based on past activity (previously "Travel")`; const result = getForReportAction({reportAction, policyID: report.policyID}); @@ -719,7 +723,8 @@ describe('ModifiedExpenseMessage', () => { }; it('returns the correct text message without attribution', () => { - const expectedResult = `changed the category to "Travel" (previously "Food")`; + // Note: Category is capitalized when source attribution is present + const expectedResult = `changed the Category to "Travel" (previously "Food")`; const result = getForReportAction({reportAction, policyID: report.policyID}); From 432737c385a9a88173e321c3cce7c9bfe1f6e2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Tue, 18 Nov 2025 13:00:34 -0600 Subject: [PATCH 04/12] Enhance ModifiedExpenseMessage tests to verify policyID casing preservation for admin users and update test descriptions for clarity --- tests/unit/ModifiedExpenseMessageTest.ts | 37 ++++++++++++++++++++---- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index 0bb2f3e234b7f..d3ff193553183 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -1,9 +1,12 @@ import {getForReportAction, 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 import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import {translate} from '@src/libs/Localize'; +import type {Policy} from '@src/types/onyx'; import createRandomReportAction from '../utils/collections/reportActions'; import {createRandomReport} from '../utils/collections/reports'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -620,7 +623,6 @@ describe('ModifiedExpenseMessage', () => { }; it('returns the correct text message with AI attribution', () => { - // Note: Category is capitalized when source attribution is present const expectedResult = `changed the Category based on past activity to "Travel" (previously "Food")`; const result = getForReportAction({reportAction, policyID: report.policyID}); @@ -640,14 +642,40 @@ describe('ModifiedExpenseMessage', () => { }, }; - it('returns the correct text message with MCC attribution', () => { - // Note: Category is capitalized when MCC attribution is used (to preserve HTML link casing) + 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 = getForReportAction({reportAction, policyID: report.policyID}); expect(result).toEqual(expectedResult); }); + + it('preserves policyID casing when generating workspace rules link for admin', () => { + // This test verifies that the policyID casing is preserved in the generated URL. + // The shouldConvertToLowercase: !source parameter prevents buildMessageFragmentForValue + // from calling .toLowerCase() on the entire HTML anchor tag, which would corrupt + // the policyID in the href attribute and cause navigation to fail. + + const mockPolicy: Policy = { + id: 'AbC123XyZ789', // Mixed case to verify exact preservation + name: 'Test Policy', + role: CONST.POLICY.ROLE.ADMIN, + type: CONST.POLICY.TYPE.TEAM, + owner: 'test@example.com', + outputCurrency: 'USD', + isPolicyExpenseChatEnabled: true, + }; + + jest.spyOn(PolicyUtils, 'getPolicy').mockReturnValue(mockPolicy); + jest.spyOn(PolicyUtils, 'isPolicyAdmin').mockReturnValue(true); + + const result = getForReportAction({reportAction, policyID: mockPolicy.id}); + + // 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'); + }); }); describe('when the category is set with AI attribution', () => { @@ -662,7 +690,6 @@ describe('ModifiedExpenseMessage', () => { }; it('returns the correct text message with AI attribution', () => { - // Note: Category is capitalized when source attribution is present const expectedResult = `set the Category based on past activity to "Travel"`; const result = getForReportAction({reportAction, policyID: report.policyID}); @@ -683,7 +710,6 @@ describe('ModifiedExpenseMessage', () => { }; it('returns the correct text message with AI attribution', () => { - // Note: Category is capitalized when source attribution is present const expectedResult = `removed the Category based on past activity (previously "Travel")`; const result = getForReportAction({reportAction, policyID: report.policyID}); @@ -723,7 +749,6 @@ describe('ModifiedExpenseMessage', () => { }; it('returns the correct text message without attribution', () => { - // Note: Category is capitalized when source attribution is present const expectedResult = `changed the Category to "Travel" (previously "Food")`; const result = getForReportAction({reportAction, policyID: report.policyID}); From 106fa9a07e04757938b1ecc5e68ac58c747f4cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Tue, 18 Nov 2025 13:50:04 -0600 Subject: [PATCH 05/12] Refactor ModifiedExpenseMessageTest for improved readability by removing unnecessary whitespace and ensuring consistent formatting of policyID in test cases. --- tests/unit/ModifiedExpenseMessageTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index d3ff193553183..c1fe141c78f90 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -655,9 +655,9 @@ describe('ModifiedExpenseMessage', () => { // The shouldConvertToLowercase: !source parameter prevents buildMessageFragmentForValue // from calling .toLowerCase() on the entire HTML anchor tag, which would corrupt // the policyID in the href attribute and cause navigation to fail. - + const mockPolicy: Policy = { - id: 'AbC123XyZ789', // Mixed case to verify exact preservation + id: 'AbC123XyZ789', // Mixed case to verify exact preservation name: 'Test Policy', role: CONST.POLICY.ROLE.ADMIN, type: CONST.POLICY.TYPE.TEAM, From 5ab4d8a29e802890c4a8e64eda1ceb8c0acdef5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Tue, 18 Nov 2025 13:58:47 -0600 Subject: [PATCH 06/12] Update translations for 'basedOnMCC' in multiple languages to improve clarity and consistency in messaging. --- src/languages/de.ts | 2 +- src/languages/fr.ts | 2 +- src/languages/it.ts | 3 ++- src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 2 +- src/languages/zh-hans.ts | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index f4db04e474379..c49bf8d60a22b 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1335,7 +1335,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `änderte das ${translatedChangedField} zu ${newMerchant} (zuvor ${oldMerchant}), wodurch der Betrag auf ${newAmountToDisplay} aktualisiert wurde (zuvor ${oldAmountToDisplay})`, basedOnAI: 'basierend auf früheren Aktivitäten', - basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `basierend auf Arbeitsbereichsregeln` : 'basierend auf Arbeitsbereichsregel'), + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `gemäß den Regeln des Arbeitsbereichs` : 'gemäß der Arbeitsbereichsregel'), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `für ${comment}` : 'Ausgabe'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `Rechnungsbericht Nr. ${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} gesendet${comment ? `für ${comment}` : ''}`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 4669ee9220aea..e1ec647561c28 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1339,7 +1339,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `a changé le ${translatedChangedField} en ${newMerchant} (précédemment ${oldMerchant}), ce qui a mis à jour le montant à ${newAmountToDisplay} (précédemment ${oldAmountToDisplay})`, basedOnAI: "basé sur l'activité passée", - basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `basé sur les règles de l'espace de travail` : "basé sur la règle de l'espace de travail"), + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `selon les règles de l'espace de travail` : 'selon la règle de l’espace de travail'), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `pour ${comment}` : 'dépense'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `Rapport de Facture n°${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} envoyé${comment ? `pour ${comment}` : ''}`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 5b9f03687a6cd..308259f0115d6 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1333,7 +1333,8 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `ha cambiato il ${translatedChangedField} in ${newMerchant} (precedentemente ${oldMerchant}), il che ha aggiornato l'importo a ${newAmountToDisplay} (precedentemente ${oldAmountToDisplay})`, basedOnAI: 'basato su attività passate', - basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `basato su regole dello spazio di lavoro` : 'basato su regola dello spazio di lavoro'), + basedOnMCC: ({rulesLink}: {rulesLink: string}) => + rulesLink ? `in base alle regole dello spazio di lavoro` : 'in base alla regola dello spazio di lavoro', threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `per ${comment}` : 'spesa'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `Rapporto Fattura n. ${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} inviato${comment ? `per ${comment}` : ''}`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 2aa9799ea5ec9..96e5051aa064d 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1335,7 +1335,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `${translatedChangedField}を${newMerchant}に変更しました(以前は${oldMerchant})、これにより金額が${newAmountToDisplay}に更新されました(以前は${oldAmountToDisplay})。`, basedOnAI: '過去のアクティビティに基づく', - basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `ワークスペースルールに基づく` : 'ワークスペースルールに基づく'), + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `ワークスペースのルールに基づいて` : 'ワークスペースのルールに基づく'), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `${comment}用` : '経費'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `請求書レポート #${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} 送信済み${comment ? `${comment} のために` : ''}`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 53ccab58d14f1..a36413de58244 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1333,7 +1333,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `veranderde de ${translatedChangedField} naar ${newMerchant} (voorheen ${oldMerchant}), wat het bedrag bijwerkte naar ${newAmountToDisplay} (voorheen ${oldAmountToDisplay})`, basedOnAI: 'op basis van eerdere activiteit', - basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `op basis van werkruimteregels` : 'op basis van werkruimteregel'), + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `op basis van werkruimteregels` : 'gebaseerd op werkruimteregel'), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `voor ${comment}` : 'uitgave'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `Factuurrapport #${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} verzonden${comment ? `voor ${comment}` : ''}`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 0a391c9520153..ab49d244e03c7 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1332,7 +1332,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `zmienił ${translatedChangedField} na ${newMerchant} (wcześniej ${oldMerchant}), co zaktualizowało kwotę na ${newAmountToDisplay} (wcześniej ${oldAmountToDisplay})`, basedOnAI: 'na podstawie wcześniejszej aktywności', - basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `na podstawie reguł przestrzeni roboczej` : 'na podstawie reguły przestrzeni roboczej'), + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `na podstawie zasad obszaru roboczego` : 'na podstawie reguły obszaru roboczego'), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `dla ${comment}` : 'wydatek'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `Raport faktury nr ${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} wysłano${comment ? `dla ${comment}` : ''}`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 50da26db4df44..f2729eb88b26a 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1313,7 +1313,7 @@ const translations: TranslationDeepObject = { updatedTheDistanceMerchant: ({translatedChangedField, newMerchant, oldMerchant, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceMerchantParams) => `将${translatedChangedField}更改为${newMerchant}(之前为${oldMerchant}),这更新了金额为${newAmountToDisplay}(之前为${oldAmountToDisplay})`, basedOnAI: '基于过去的活动', - basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `基于工作空间规则` : '基于工作空间规则'), + basedOnMCC: ({rulesLink}: {rulesLink: string}) => (rulesLink ? `基于 工作区规则` : '基于工作区规则'), threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `为${comment}` : '费用'}`, invoiceReportName: ({linkedReportID}: OriginalMessage) => `发票报告 #${linkedReportID}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} 已发送${comment ? `对于${comment}` : ''}`, From f2fc723ef91376db243fd9970f2fdf92ea841bd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Thu, 20 Nov 2025 13:12:46 -0600 Subject: [PATCH 07/12] Refactor Onyx connection to use connectWithoutView for POLICY_TAGS and SESSION --- 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 24873e4df45cc..dbc9fbba74b0c 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -21,7 +21,7 @@ import {buildReportNameFromParticipantNames, getPolicyExpenseChatName, getPolicy import {getFormattedAttendees, getTagArrayFromName} from './TransactionUtils'; let allPolicyTags: OnyxCollection = {}; -Onyx.connect({ +Onyx.connectWithoutView({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, waitForCollectionCallback: true, callback: (value) => { @@ -37,7 +37,7 @@ let environmentURL: string; getEnvironmentURL().then((url: string) => (environmentURL = url)); let currentUserLogin = ''; -Onyx.connect({ +Onyx.connectWithoutView({ key: ONYXKEYS.SESSION, callback: (value) => { // When signed out, value is undefined From c41117d4acf7b15eddc22e124a87816555f85a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Thu, 20 Nov 2025 13:35:22 -0600 Subject: [PATCH 08/12] Refactor report action handling to use policy directly instead of policyID, improving type safety and clarity in context menus. --- src/libs/ModifiedExpenseMessage.ts | 8 +++----- .../report/ContextMenu/BaseReportActionContextMenu.tsx | 2 ++ src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index dbc9fbba74b0c..a47a327d0e0e4 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -5,7 +5,7 @@ import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PolicyTagLists, Report, ReportAction} from '@src/types/onyx'; +import type {Policy, PolicyTagLists, Report, ReportAction} from '@src/types/onyx'; import {getDecodedCategoryName} from './CategoryUtils'; import {convertToDisplayString} from './CurrencyUtils'; import DateUtils from './DateUtils'; @@ -440,13 +440,13 @@ function getForReportAction({ */ function getForReportActionTemp({ reportAction, - policyID, + policy, movedFromReport, movedToReport, policyTags, }: { reportAction: OnyxEntry; - policyID?: string | undefined; + policy?: OnyxEntry; movedFromReport?: OnyxEntry; movedToReport?: OnyxEntry; policyTags: OnyxEntry; @@ -536,8 +536,6 @@ function getForReportActionTemp({ // eslint-disable-next-line @typescript-eslint/no-deprecated categoryLabel += ` ${translateLocal('iou.basedOnAI')}`; } 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); // For admins, create a hyperlink to the workspace rules page diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 0ba3843d94fa0..4cbef5fe0fc4a 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -159,6 +159,7 @@ function BaseReportActionContextMenu({ const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`, {canBeMissing: true}); const isOriginalReportArchived = useReportIsArchived(originalReportID); const policyID = report?.policyID; + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: true}); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); const [movedFromReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(reportAction, CONST.REPORT.MOVE_TYPE.FROM)}`, {canBeMissing: true}); @@ -372,6 +373,7 @@ function BaseReportActionContextMenu({ movedFromReport, movedToReport, getLocalDateFromDatetime, + policy, policyTags, translate, }; diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 0b6724d8e9eda..7fd68ee7ba09b 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -213,6 +213,7 @@ type ContextMenuActionPayload = { movedFromReport?: OnyxEntry; movedToReport?: OnyxEntry; getLocalDateFromDatetime: LocaleContextProps['getLocalDateFromDatetime']; + policy?: OnyxEntry; policyTags: OnyxEntry; translate: LocalizedTranslate; }; @@ -537,6 +538,7 @@ const ContextMenuActions: ContextMenuAction[] = [ movedToReport, childReport, getLocalDateFromDatetime, + policy, policyTags, translate, }, @@ -559,7 +561,7 @@ const ContextMenuActions: ContextMenuAction[] = [ } else if (isModifiedExpenseAction(reportAction)) { const modifyExpenseMessage = getForReportActionTemp({ reportAction, - policyID: report?.policyID, + policy, movedFromReport, movedToReport, policyTags, From cfbb31a8b29e19218a9efde6f4aa9ab8a7dcabc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Thu, 20 Nov 2025 13:35:41 -0600 Subject: [PATCH 09/12] Update test case for ModifiedExpenseMessage to clarify workspace rules link generation for admin --- tests/unit/ModifiedExpenseMessageTest.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index c1fe141c78f90..5d82532aaa518 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -650,8 +650,7 @@ describe('ModifiedExpenseMessage', () => { expect(result).toEqual(expectedResult); }); - it('preserves policyID casing when generating workspace rules link for admin', () => { - // This test verifies that the policyID casing is preserved in the generated URL. + it('returns the correct workspace rules link for admin', () => { // The shouldConvertToLowercase: !source parameter prevents buildMessageFragmentForValue // from calling .toLowerCase() on the entire HTML anchor tag, which would corrupt // the policyID in the href attribute and cause navigation to fail. From fb570a75505cc62c7b1923bcbff80c5862c601c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Mon, 24 Nov 2025 14:49:57 -0600 Subject: [PATCH 10/12] Refactor category label handling in getForReportActionTemp function to ensure consistent lowercase formatting and adjust source attribution logic. --- 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 fcf6de44d5a41..916502daa0812 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -539,7 +539,7 @@ function getForReportActionTemp({ const hasModifiedCategory = isReportActionOriginalMessageAnObject && 'oldCategory' in reportActionOriginalMessage && 'category' in reportActionOriginalMessage; if (hasModifiedCategory) { // eslint-disable-next-line @typescript-eslint/no-deprecated - let categoryLabel = translateLocal('common.category'); + let categoryLabel = translateLocal('common.category').toLowerCase(); // Add attribution suffix based on source if (reportActionOriginalMessage?.source === CONST.CATEGORY_SOURCE.AI) { @@ -568,7 +568,7 @@ function getForReportActionTemp({ removalFragments, changeFragments, // Don't convert to lowercase when we have source attribution (to preserve any HTML links) - !reportActionOriginalMessage?.source, + false, ); } From 2260f3aa30d2830ac76fed751c6bb167c2f9461f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Mon, 24 Nov 2025 14:59:40 -0600 Subject: [PATCH 11/12] Update category label handling in getForReportAction function to ensure consistent lowercase formatting and modify source attribution logic to prevent unnecessary conversions. --- 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 916502daa0812..ebb0fceb76fdd 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -292,7 +292,7 @@ function getForReportAction({ const hasModifiedCategory = isReportActionOriginalMessageAnObject && 'oldCategory' in reportActionOriginalMessage && 'category' in reportActionOriginalMessage; if (hasModifiedCategory) { // eslint-disable-next-line @typescript-eslint/no-deprecated - let categoryLabel = translateLocal('common.category'); + let categoryLabel = translateLocal('common.category').toLowerCase(); // Add attribution suffix based on source if (reportActionOriginalMessage?.source === CONST.CATEGORY_SOURCE.AI) { @@ -323,7 +323,7 @@ function getForReportAction({ removalFragments, changeFragments, // Don't convert to lowercase when we have source attribution (to preserve any HTML links) - !reportActionOriginalMessage?.source, + false, ); } From 74961e57caf28f5180c86b8f12bbc06170f8d42a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Ch=C3=A1vez?= Date: Tue, 25 Nov 2025 16:12:55 -0600 Subject: [PATCH 12/12] Fix capitalization in ModifiedExpenseMessageTest expected results --- tests/unit/ModifiedExpenseMessageTest.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index 5efe1336bbc8b..d3825740f58e7 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -664,7 +664,7 @@ describe('ModifiedExpenseMessage', () => { }; it('returns the correct text message with AI attribution', () => { - const expectedResult = `changed the Category based on past activity to "Travel" (previously "Food")`; + const expectedResult = `changed the category based on past activity to "Travel" (previously "Food")`; const result = getForReportAction({reportAction, policyID: report.policyID}); @@ -684,7 +684,7 @@ describe('ModifiedExpenseMessage', () => { }; 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 expectedResult = `changed the category based on workspace rule to "Travel" (previously "Food")`; const result = getForReportAction({reportAction, policyID: report.policyID}); @@ -730,7 +730,7 @@ describe('ModifiedExpenseMessage', () => { }; it('returns the correct text message with AI attribution', () => { - const expectedResult = `set the Category based on past activity to "Travel"`; + const expectedResult = `set the category based on past activity to "Travel"`; const result = getForReportAction({reportAction, policyID: report.policyID}); @@ -750,7 +750,7 @@ describe('ModifiedExpenseMessage', () => { }; it('returns the correct text message with AI attribution', () => { - const expectedResult = `removed the Category based on past activity (previously "Travel")`; + const expectedResult = `removed the category based on past activity (previously "Travel")`; const result = getForReportAction({reportAction, policyID: report.policyID}); @@ -789,7 +789,7 @@ describe('ModifiedExpenseMessage', () => { }; it('returns the correct text message without attribution', () => { - const expectedResult = `changed the Category to "Travel" (previously "Food")`; + const expectedResult = `changed the category to "Travel" (previously "Food")`; const result = getForReportAction({reportAction, policyID: report.policyID});