From 169b6b2a1585fa3ffe1e98c12495ad7cdd50be95 Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Thu, 10 Sep 2026 03:12:52 +0000 Subject: [PATCH 01/20] Render the Concierge automatic distance rate change message Register the CONCIERGEAUTOSELECTDISTANCERATE action type so it is no longer filtered out by shouldReportActionBeVisible, and build its copy in a single shared helper that the report, the LHN preview, copy to clipboard and the thread title all call, so the message is localized on every surface instead of falling back to the English text the backend writes. Co-authored-by: dmkt9 --- src/CONST/index.ts | 5 + src/languages/en.ts | 5 + src/libs/ReportActionsUtils.ts | 29 +++++ src/libs/ReportAlternateTextUtils.ts | 5 + src/libs/ReportNameUtils.ts | 5 + .../report/ContextMenu/ContextMenuActions.tsx | 3 + .../actionContents/ActionContentRouter.tsx | 8 ++ src/types/onyx/OriginalMessage.ts | 21 +++- tests/unit/ReportActionsUtilsTest.ts | 114 ++++++++++++++++++ tests/unit/ReportAlternateTextUtilsTest.ts | 38 ++++++ 10 files changed, 232 insertions(+), 1 deletion(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 76b24d23a573..4beeec0002ab 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1712,6 +1712,7 @@ const CONST = { MERGED_WITH_CASH_TRANSACTION: 'MERGEDWITHCASHTRANSACTION', MODIFIED_EXPENSE: 'MODIFIEDEXPENSE', CONCIERGE_AUTO_MATCH_VENDOR: 'CONCIERGEAUTOMATCHVENDOR', + CONCIERGE_AUTO_SELECT_DISTANCE_RATE: 'CONCIERGEAUTOSELECTDISTANCERATE', MOVED: 'MOVED', MOVED_TRANSACTION: 'MOVEDTRANSACTION', UNREPORTED_TRANSACTION: 'UNREPORTEDTRANSACTION', @@ -1939,6 +1940,10 @@ const CONST = { ACCEPT: 'accept', DECLINE: 'decline', }, + CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE: { + WORKSPACE_CHANGED: 'workspaceChanged', + REPORT_MOVED: 'reportMoved', + }, ARCHIVE_REASON: { DEFAULT: 'default', ACCOUNT_CLOSED: 'accountClosed', diff --git a/src/languages/en.ts b/src/languages/en.ts index 3411b8da2b85..4080fd6e0309 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1908,6 +1908,11 @@ const translations = { correctRateError: 'Fix the rate error and try again.', AskToExplain: `. Explain`, conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge matched this expense to ${vendorName}`, + // @context "rate" is the distance rate of an expense (an amount of money per mile or kilometer), not a price, a fee or a rating. "rate" is lowercase because the sentence continues a system message. + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `rate updated to ${rate} for the new workspace - ${policyName}`, + // @context "rate" is the distance rate of an expense (an amount of money per mile or kilometer), not a price, a fee or a rating. Shown when the expense report was moved to another workspace, so the workspace of the report is the one that changed. + conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => + `rate updated to ${rate} for the new report’s workspace - ${policyName}`, rulesModifiedFields: { reimbursable: (value: boolean) => (value ? 'marked the expense as "reimbursable"' : 'marked the expense as "non-reimbursable"'), billable: (value: boolean) => (value ? 'marked the expense as "billable"' : 'marked the expense as "non-billable"'), diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index b08104be8ee8..2b979b43271d 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -2508,6 +2508,11 @@ function getReportActionMessageFragments(translate: LocalizedTranslate, action: return [{text: message, html: `${message}`, type: 'COMMENT'}]; } + if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE)) { + const message = getConciergeAutoSelectDistanceRateMessage(translate, action); + return [{text: Parser.htmlToText(message), html: `${message}`, type: 'COMMENT'}]; + } + if (isDynamicExternalWorkflowSubmitFailedAction(action)) { const failedSubmitReason = getDynamicExternalWorkflowSubmitFailedActionMessage(translate, action); return [{text: failedSubmitReason, html: `${failedSubmitReason}`, type: 'COMMENT'}]; @@ -3409,6 +3414,29 @@ function getWorkspaceCustomUnitRateUpdatedMessage(translate: LocalizedTranslate, return getReportActionText(action); } +/** + * Builds the Concierge system message explaining that the distance rate of an expense was updated automatically. + * It is the single source of the copy for every surface (the report, the LHN preview, copy to clipboard), so the message is localized everywhere. + */ +function getConciergeAutoSelectDistanceRateMessage(translate: LocalizedTranslate, action: ReportAction): string { + const {rate, currency, unit, policyName, changeType} = getOriginalMessage(action as ReportAction) ?? {}; + + if (!rate || !currency || !unit || !policyName) { + return getReportActionText(action); + } + + // The rate is stored in the CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET scale, which is the scale convertAmountToDisplayString divides by, so 67 is displayed as $0.67. + const formattedRate = `${convertAmountToDisplayString(rate, currency)} / ${unit}`; + // The workspace name is interpolated into a message that is rendered as HTML, so encode it to prevent a name containing markup from being parsed as HTML. + const encodedPolicyName = Str.htmlEncode(policyName); + + if (changeType === CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.REPORT_MOVED) { + return translate('iou.conciergeAutoSelectedDistanceRateForMovedReport', {rate: formattedRate, policyName: encodedPolicyName}); + } + + return translate('iou.conciergeAutoSelectedDistanceRate', {rate: formattedRate, policyName: encodedPolicyName}); +} + function getWorkspaceCustomUnitRateDeletedMessage(translate: LocalizedTranslate, action: ReportAction): string { const {customUnitName, rateName} = getOriginalMessage(action as ReportAction) ?? {}; if (customUnitName && rateName) { @@ -5030,6 +5058,7 @@ export { getRemovedFromApprovalChainMessage, getDemotedFromWorkspaceMessage, getDynamicExternalWorkflowRoutedMessage, + getConciergeAutoSelectDistanceRateMessage, getReportAction, getReportActionHtml, getReportActionMessage, diff --git a/src/libs/ReportAlternateTextUtils.ts b/src/libs/ReportAlternateTextUtils.ts index 69a94e66ad9e..d637c38874a6 100644 --- a/src/libs/ReportAlternateTextUtils.ts +++ b/src/libs/ReportAlternateTextUtils.ts @@ -53,6 +53,7 @@ import { getCombinedReportActions, getCompanyAddressUpdateMessage, getCompanyCardConnectionBrokenMessage, + getConciergeAutoSelectDistanceRateMessage, getCurrencyConversionFeeMessage, getCurrencyDefaultTaxUpdateMessage, getCustomTaxNameUpdateMessage, @@ -757,6 +758,8 @@ function getLastMessageTextForReport({ lastMessageTextFromReport = Parser.htmlToText(getActionableMentionWhisperMessage(translate, lastReportAction, getPersonalDetailsListByIDs(targetAccountIDs, personalDetails))); } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED)) { lastMessageTextFromReport = getDynamicExternalWorkflowRoutedMessage(lastReportAction, translate); + } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE)) { + lastMessageTextFromReport = Parser.htmlToText(getConciergeAutoSelectDistanceRateMessage(translate, lastReportAction)); } if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT)) { lastMessageTextFromReport = getPolicyChangeLogMaxExpenseAmountMessage(translate, lastReportAction, convertToDisplayString); @@ -1486,6 +1489,8 @@ function getReportAlternateText({ alternateText = translate('iou.reopened'); } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.TRAVEL_UPDATE)) { alternateText = getTravelUpdateMessage(translate, lastAction); + } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE)) { + alternateText = Parser.htmlToText(getConciergeAutoSelectDistanceRateMessage(translate, lastAction)); } else if ( isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL) || isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.REROUTE) || diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index a796669c0b06..06b4385324ac 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -49,6 +49,7 @@ import { getChangedApproverActionMessage, getCompanyAddressUpdateMessage, getCompanyCardConnectionBrokenMessage, + getConciergeAutoSelectDistanceRateMessage, getCreatedReportForUnapprovedTransactionsMessage, getCrossBorderReimbursedMessage, getCurrencyConversionFeeMessage, @@ -873,6 +874,10 @@ function computeReportNameBasedOnReportAction({ return getTravelUpdateMessage(translate, parentReportAction); } + if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE)) { + return Parser.htmlToText(getConciergeAutoSelectDistanceRateMessage(translate, parentReportAction)); + } + if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CUSTOM_UNIT_RATE)) { return getWorkspaceCustomUnitRateAddedMessage(translate, dateFnsLocale, parentReportAction); } diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx index a4b996145a09..4dd15fb7803e 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx @@ -41,6 +41,7 @@ import { getChangedApproverActionMessage, getCompanyAddressUpdateMessage, getCompanyCardConnectionBrokenMessage, + getConciergeAutoSelectDistanceRateMessage, getCreatedReportForUnapprovedTransactionsMessage, getCurrencyConversionFeeMessage, getCurrencyDefaultTaxUpdateMessage, @@ -1360,6 +1361,8 @@ const ContextMenuActions: ContextMenuAction[] = [ setClipboardMessage(getUpdatedCardFeedStatementPeriodMessage(translate, reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.TRAVEL_UPDATE)) { setClipboardMessage(getTravelUpdateMessage(translate, reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE)) { + setClipboardMessage(getConciergeAutoSelectDistanceRateMessage(translate, reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUDIT_RATE)) { setClipboardMessage(getUpdatedAuditRateMessage(translate, reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_APPROVER_RULE)) { diff --git a/src/pages/inbox/report/actionContents/ActionContentRouter.tsx b/src/pages/inbox/report/actionContents/ActionContentRouter.tsx index 0e8f9d76f666..b9a43b6e3683 100644 --- a/src/pages/inbox/report/actionContents/ActionContentRouter.tsx +++ b/src/pages/inbox/report/actionContents/ActionContentRouter.tsx @@ -20,6 +20,7 @@ import { getChangedApproverActionMessage, getCommuterExclusionMessage, getCompanyCardConnectionBrokenMessage, + getConciergeAutoSelectDistanceRateMessage, getDelegateSubmitMessage, getForwardedReportActionMessage, getIOUReportIDFromReportActionPreview, @@ -359,6 +360,13 @@ function ActionContentRouter({ ); } + if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE)) { + return ( + + ${getConciergeAutoSelectDistanceRateMessage(translate, action)}`} /> + + ); + } if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.TRAVEL_NUDGE)) { return ( diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index f086bfa702de..c7fed657173e 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -7,7 +7,7 @@ import type {CardID} from './Card'; import type {PolicyRuleTaxRate} from './ExpenseRule'; import type {Attendee} from './IOU'; import type {OldDotOriginalMessageMap} from './OldDotAction'; -import type {AllConnectionName} from './Policy'; +import type {AllConnectionName, Unit} from './Policy'; import type {PolicyChangeLogCopyReportActionNames} from './ReportAction'; import type ReportActionName from './ReportActionName'; import type {Reservation, TransactionCommentVendor} from './Transaction'; @@ -955,6 +955,24 @@ type OriginalMessageConciergeAutoMatchVendor = { reasoning?: string; }; +/** Model of `concierge auto select distance rate` report action — emitted when the distance rate of an expense is changed automatically because its workspace changed. */ +type OriginalMessageConciergeAutoSelectDistanceRate = { + /** The new rate, in the `CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET` scale (e.g. 67 renders as $0.67) */ + rate?: number; + + /** Currency of the new rate */ + currency?: string; + + /** Distance unit of the new rate */ + unit?: Unit; + + /** Name of the workspace the new rate belongs to */ + policyName?: string; + + /** Whether the workspace of the expense changed directly, or the report the expense belongs to was moved to another workspace */ + changeType?: ValueOf; +}; + /** Policy rules modified fields. Each member holds the new value the rule wrote, not the current one */ type PolicyRulesModifiedFields = { merchant?: string; @@ -1609,6 +1627,7 @@ type OriginalMessageMap = { [CONST.REPORT.ACTIONS.TYPE.MERGED_WITH_CASH_TRANSACTION]: never; [CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE]: OriginalMessageModifiedExpense; [CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_MATCH_VENDOR]: OriginalMessageConciergeAutoMatchVendor; + [CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE]: OriginalMessageConciergeAutoSelectDistanceRate; [CONST.REPORT.ACTIONS.TYPE.MOVED]: OriginalMessageMoved; [CONST.REPORT.ACTIONS.TYPE.MOVED_TRANSACTION]: OriginalMessageMovedTransaction; [CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION]: OriginalMessageUnreportedTransaction; diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index a9e7a2d9b77b..b3217e5992f0 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -4,6 +4,7 @@ import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import getReportURLForCurrentContext from '@libs/Navigation/helpers/getReportURLForCurrentContext'; import {setHasRadio} from '@libs/NetworkState'; +import Parser from '@libs/Parser'; import {isExpenseReport} from '@libs/ReportUtils'; import IntlStore from '@src/languages/IntlStore'; @@ -88,6 +89,8 @@ import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatch type TakeControlAction = ReportAction; type TakeControlOriginalMessageFixture = NonNullable; +type ConciergeAutoSelectDistanceRateAction = ReportAction; + type LegacyReportActionFields = { message?: string; originalMessage?: string; @@ -1582,6 +1585,117 @@ describe('ReportActionsUtils', () => { }); }); + describe('getConciergeAutoSelectDistanceRateMessage', () => { + function buildConciergeAutoSelectDistanceRateAction( + originalMessage: ConciergeAutoSelectDistanceRateAction['originalMessage'], + backendText = 'rate updated by the backend', + ): ConciergeAutoSelectDistanceRateAction { + return { + actionName: CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE, + reportActionID: 'concierge-auto-select-distance-rate-1', + created: '2026-09-10 12:00:00.000', + message: [{type: CONST.REPORT.MESSAGE.TYPE.COMMENT, text: backendText, html: backendText}], + originalMessage, + }; + } + + it('should describe a direct workspace change', () => { + // Given an action whose workspace changed directly + const action = buildConciergeAutoSelectDistanceRateAction({ + rate: 67, + currency: 'USD', + unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + policyName: "Hal's Burgers", + changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.WORKSPACE_CHANGED, + }); + + // When building the message + const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); + + // Then it should name the new workspace and the formatted rate + expect(Parser.htmlToText(message)).toBe("rate updated to $0.67 / mi for the new workspace - Hal's Burgers"); + }); + + it('should describe a report that moved to another workspace', () => { + // Given an action triggered by the report moving to another workspace + const action = buildConciergeAutoSelectDistanceRateAction({ + rate: 67, + currency: 'USD', + unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + policyName: "Hal's Burgers", + changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.REPORT_MOVED, + }); + + // When building the message + const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); + + // Then it should say the workspace of the report changed + expect(Parser.htmlToText(message)).toBe("rate updated to $0.67 / mi for the new report’s workspace - Hal's Burgers"); + }); + + it('should escape a workspace name containing markup', () => { + // Given a workspace name containing HTML + const action = buildConciergeAutoSelectDistanceRateAction({ + rate: 67, + currency: 'USD', + unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + policyName: 'Hal', + changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.WORKSPACE_CHANGED, + }); + + // When building the message + const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); + + // Then the markup should be encoded rather than rendered + expect(message).toContain('<strong>Hal</strong>'); + expect(message).not.toContain(''); + }); + + it('should fall back to the text of the action when the rate details are missing', () => { + // Given an action without the rate details + const backendText = 'rate updated by the backend'; + const action = buildConciergeAutoSelectDistanceRateAction({policyName: "Hal's Burgers"}, backendText); + + // When building the message + const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); + + // Then it should fall back to the text the backend provided + expect(message).toBe(backendText); + }); + + it('should be used for the message fragments of the action', () => { + // Given a CONCIERGE_AUTO_SELECT_DISTANCE_RATE action + const action = buildConciergeAutoSelectDistanceRateAction({ + rate: 67, + currency: 'USD', + unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + policyName: "Hal's Burgers", + changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.WORKSPACE_CHANGED, + }); + + // When getting the message fragments of the action + const fragments = ReportActionsUtils.getReportActionMessageFragments(translateLocal, action); + + // Then they should be built from the same message + const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); + expect(fragments).toEqual([{text: Parser.htmlToText(message), html: `${message}`, type: 'COMMENT'}]); + }); + + it('should be visible in the report', () => { + // Given a CONCIERGE_AUTO_SELECT_DISTANCE_RATE action + const action = buildConciergeAutoSelectDistanceRateAction({ + rate: 67, + currency: 'USD', + unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + policyName: "Hal's Burgers", + changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.WORKSPACE_CHANGED, + }); + + // Then the action should not be filtered out as an unsupported action type + expect(ReportActionsUtils.shouldReportActionBeVisible(action, action.reportActionID, true)).toBe(true); + }); + }); + describe('getReportActionText', () => { it('should return the backend-provided CARDFROZEN text', () => { const cardFrozenMessage = 'A A froze their Expensify Card (ending in 1384). New transactions will be declined until the card is unfrozen.'; diff --git a/tests/unit/ReportAlternateTextUtilsTest.ts b/tests/unit/ReportAlternateTextUtilsTest.ts index 5c6d6094aad0..96d4f5f02556 100644 --- a/tests/unit/ReportAlternateTextUtilsTest.ts +++ b/tests/unit/ReportAlternateTextUtilsTest.ts @@ -1276,6 +1276,44 @@ describe('ReportAlternateTextUtils', () => { }); expect(lastMessage).toBe(getCurrencyDefaultTaxUpdateMessage(translateLocal, action)); }); + it('CONCIERGE_AUTO_SELECT_DISTANCE_RATE action', async () => { + // Given a report whose last action is an automatic distance rate change + const report: Report = createRandomReport(0, undefined); + const action: ReportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE, + message: [{type: 'COMMENT', text: 'rate updated by the backend'}], + originalMessage: { + rate: 67, + currency: 'USD', + unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + policyName: "Hal's Burgers", + changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.WORKSPACE_CHANGED, + }, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [action.reportActionID]: action, + }); + + // When getting the last message text of the report + const lastMessage = getLastMessageTextForReport({ + dateFnsLocale: undefined, + convertToDisplayString, + conciergeReportID: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + personalDetails: undefined, + translate: translateLocal, + report, + lastActorDetails: null, + policy: undefined, + isReportArchived: false, + + currentUserLogin: CURRENT_USER_LOGIN, + }); + + // Then it should be built from the translation rather than the text the backend provided + expect(lastMessage).toBe("rate updated to $0.67 / mi for the new workspace - Hal's Burgers"); + }); it('ADD_AGENT_RULE action', async () => { const report: Report = createRandomReport(0, undefined); const action: ReportAction = { From 629eafe591670abff63d16187b325a54630fc3b9 Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Thu, 10 Sep 2026 03:26:25 +0000 Subject: [PATCH 02/20] Add generated translations for the Concierge distance rate keys and apply oxfmt Co-authored-by: dmkt9 --- src/languages/de.ts | 3 +++ src/languages/el.ts | 4 ++++ src/languages/en.ts | 3 +-- src/languages/es.ts | 3 +++ src/languages/fr.ts | 3 +++ src/languages/it.ts | 3 +++ src/languages/ja.ts | 3 +++ src/languages/nl.ts | 3 +++ src/languages/pl.ts | 3 +++ src/languages/pt-BR.ts | 3 +++ src/languages/zh-hans.ts | 2 ++ 11 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 64369fbe6d64..fe524b6e3b1d 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1862,6 +1862,9 @@ const translations: TranslationDeepObject = { prompt: 'Aktivieren Sie Tags im Workspace, um die Ausgabendetails zu bearbeiten oder den Tag aus dieser Ausgabe zu löschen.', confirmText: 'Tag löschen', }, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `Kilometersatz auf ${rate} für den neuen Workspace – ${policyName} aktualisiert`, + conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => + `Kilometersatz für den neuen Bericht-Arbeitsbereich auf ${rate} aktualisiert – ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/el.ts b/src/languages/el.ts index 9761562bca1f..20bc4142fe23 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -1912,6 +1912,10 @@ const translations: TranslationDeepObject = { whatIsHoldExplainDM: 'Η αναμονή είναι σαν να πατάτε «παύση» σε μία δαπάνη μέχρι να είστε έτοιμοι να τη στείλετε.', holdIsLeftBehindDM: 'Οι δεσμευμένες δαπάνες δεν θα αποσταλούν μέχρι να καταργήσετε τη δέσμευση.', unholdWhenReadyDM: 'Αποδεσμεύστε τις δαπάνες όταν είστε έτοιμοι να τις στείλετε.', + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => + `το χιλιομετρικό κόστος ενημερώθηκε σε ${rate} για τον νέο χώρο εργασίας - ${policyName}`, + conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => + `το χιλιόμετρο-κόμιστρο ενημερώθηκε σε ${rate} για τον νέο χώρο εργασίας της αναφοράς - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 4080fd6e0309..2226f9901888 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1911,8 +1911,7 @@ const translations = { // @context "rate" is the distance rate of an expense (an amount of money per mile or kilometer), not a price, a fee or a rating. "rate" is lowercase because the sentence continues a system message. conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `rate updated to ${rate} for the new workspace - ${policyName}`, // @context "rate" is the distance rate of an expense (an amount of money per mile or kilometer), not a price, a fee or a rating. Shown when the expense report was moved to another workspace, so the workspace of the report is the one that changed. - conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `rate updated to ${rate} for the new report’s workspace - ${policyName}`, + conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => `rate updated to ${rate} for the new report’s workspace - ${policyName}`, rulesModifiedFields: { reimbursable: (value: boolean) => (value ? 'marked the expense as "reimbursable"' : 'marked the expense as "non-reimbursable"'), billable: (value: boolean) => (value ? 'marked the expense as "billable"' : 'marked the expense as "non-billable"'), diff --git a/src/languages/es.ts b/src/languages/es.ts index c23f6ba2d1bf..1075ad6fa483 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1853,6 +1853,9 @@ const translations: TranslationDeepObject = { prompt: 'Habilita las etiquetas en el espacio de trabajo para editar los detalles del gasto o eliminar la etiqueta de este gasto.', confirmText: 'Eliminar etiqueta', }, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `se actualizó la tasa a ${rate} para el nuevo espacio de trabajo - ${policyName}`, + conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => + `tasa actualizada a ${rate} para el nuevo espacio de trabajo del informe: ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 6a8fd7075903..be952eb9a508 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1868,6 +1868,9 @@ const translations: TranslationDeepObject = { prompt: 'Active les tags dans l’espace de travail pour modifier les détails de la dépense ou supprimer le tag de cette dépense.', confirmText: 'Supprimer le tag', }, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `taux mis à jour à ${rate} pour le nouvel espace de travail - ${policyName}`, + conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => + `taux mis à jour à ${rate} pour le nouvel espace de travail de la note de frais - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 821601675e2e..14dfdec67cb1 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1858,6 +1858,9 @@ const translations: TranslationDeepObject = { prompt: 'Abilita le etichette nello spazio di lavoro per modificare i dettagli della spesa o eliminare l’etichetta da questa spesa.', confirmText: 'Elimina tag', }, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `tariffa aggiornata a ${rate} per il nuovo spazio di lavoro - ${policyName}`, + conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => + `tariffa aggiornata a ${rate} per il nuovo workspace del report - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 91ac703f21f7..613fe0b6d801 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1840,6 +1840,9 @@ const translations: TranslationDeepObject = { prompt: 'ワークスペースでタグを有効にすると、この経費の詳細を編集したり、この経費からタグを削除したりできます。', confirmText: 'タグを削除', }, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `新しいワークスペース「${policyName}」の距離レートが${rate}に更新されました`, + conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => + `新しいレポートのワークスペース用のレートを ${rate} に更新しました - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index d9d5feaf84d7..78df126db0cb 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1854,6 +1854,9 @@ const translations: TranslationDeepObject = { prompt: 'Schakel tags in op de werkruimte om de onkostendetails te bewerken of de tag uit deze onkosten te verwijderen.', confirmText: 'Label verwijderen', }, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `tarief bijgewerkt naar ${rate} voor de nieuwe werkruimte - ${policyName}`, + conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => + `tarief bijgewerkt naar ${rate} voor de nieuwe rapportwerkruimte - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 1347db8696ef..3a0c7d2bcbc3 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1889,6 +1889,9 @@ const translations: TranslationDeepObject = { prompt: 'Włącz tagi w przestrzeni roboczej, aby edytować szczegóły wydatku lub usunąć ten tag z tego wydatku.', confirmText: 'Usuń znacznik', }, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `stawka zaktualizowana na ${rate} dla nowego workspace – ${policyName}`, + conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => + `stawka została zaktualizowana do ${rate} dla nowej przestrzeni roboczej raportu – ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index f747ae51e755..6ac2489f0975 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1849,6 +1849,9 @@ const translations: TranslationDeepObject = { confirmText: 'Excluir categoria', }, tagDisabledAlert: {title: 'Tag desativada', prompt: 'Ative as tags no workspace para editar os detalhes da despesa ou excluir a tag desta despesa.', confirmText: 'Excluir tag'}, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `taxa atualizada para ${rate} para o novo espaço de trabalho - ${policyName}`, + conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => + `taxa atualizada para ${rate} para o novo workspace do relatório - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index d44fdfb3957a..923c78efd4b1 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1782,6 +1782,8 @@ const translations: TranslationDeepObject = { deleteConfirmationSomePendingBYOC: '您确定要删除这些报销吗?其中一些处于待处理状态,如果入账后,我们可能会再次导入。', categoryDisabledAlert: {title: '类别已禁用', prompt: '在工作区中启用类别,以编辑报销详情或从此报销中删除该类别。', confirmText: '删除类别'}, tagDisabledAlert: {title: '标签已停用', prompt: '请在工作区中启用标签,以便编辑该报销的详细信息或从此报销中删除该标签。', confirmText: '删除标签'}, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `新工作区(${policyName})的里程费率已更新为 ${rate}`, + conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => `已为新报销单的工作区(${policyName})将里程报销费率更新为 ${rate}`, }, transactionMerge: { listPage: { From 79162b18439df98bcc59b93fd1ca6d6c0825f97b Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Thu, 10 Sep 2026 03:44:53 +0000 Subject: [PATCH 03/20] Apply revised Polyglot Parrot translations for the Concierge distance rate keys Applies the translation diff from the second Polyglot Parrot run verbatim across 9 locale files. fr is unchanged by that run. Co-authored-by: dmkt9 --- src/languages/de.ts | 4 ++-- src/languages/el.ts | 5 ++--- src/languages/es.ts | 2 +- src/languages/it.ts | 4 ++-- src/languages/ja.ts | 4 ++-- src/languages/nl.ts | 2 +- src/languages/pl.ts | 4 ++-- src/languages/pt-BR.ts | 4 ++-- src/languages/zh-hans.ts | 4 ++-- 9 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index fe524b6e3b1d..8363e3144985 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1862,9 +1862,9 @@ const translations: TranslationDeepObject = { prompt: 'Aktivieren Sie Tags im Workspace, um die Ausgabendetails zu bearbeiten oder den Tag aus dieser Ausgabe zu löschen.', confirmText: 'Tag löschen', }, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `Kilometersatz auf ${rate} für den neuen Workspace – ${policyName} aktualisiert`, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `Satz auf ${rate} für den neuen Workspace „${policyName}“ aktualisiert`, conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `Kilometersatz für den neuen Bericht-Arbeitsbereich auf ${rate} aktualisiert – ${policyName}`, + `Satz pro Einheit auf ${rate} für den neuen Bericht-Arbeitsbereich aktualisiert – ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/el.ts b/src/languages/el.ts index 20bc4142fe23..8682829cd665 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -1912,10 +1912,9 @@ const translations: TranslationDeepObject = { whatIsHoldExplainDM: 'Η αναμονή είναι σαν να πατάτε «παύση» σε μία δαπάνη μέχρι να είστε έτοιμοι να τη στείλετε.', holdIsLeftBehindDM: 'Οι δεσμευμένες δαπάνες δεν θα αποσταλούν μέχρι να καταργήσετε τη δέσμευση.', unholdWhenReadyDM: 'Αποδεσμεύστε τις δαπάνες όταν είστε έτοιμοι να τις στείλετε.', - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => - `το χιλιομετρικό κόστος ενημερώθηκε σε ${rate} για τον νέο χώρο εργασίας - ${policyName}`, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `ο συντελεστής ενημερώθηκε σε ${rate} για τον νέο χώρο εργασίας - ${policyName}`, conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `το χιλιόμετρο-κόμιστρο ενημερώθηκε σε ${rate} για τον νέο χώρο εργασίας της αναφοράς - ${policyName}`, + `ο συντελεστής ενημερώθηκε σε ${rate} για τον νέο χώρο εργασίας της έκθεσης - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 1075ad6fa483..9f93666a842e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1855,7 +1855,7 @@ const translations: TranslationDeepObject = { }, conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `se actualizó la tasa a ${rate} para el nuevo espacio de trabajo - ${policyName}`, conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `tasa actualizada a ${rate} para el nuevo espacio de trabajo del informe: ${policyName}`, + `tasa actualizada a ${rate} para el nuevo espacio de trabajo del informe - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 14dfdec67cb1..5516466e6d62 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1858,9 +1858,9 @@ const translations: TranslationDeepObject = { prompt: 'Abilita le etichette nello spazio di lavoro per modificare i dettagli della spesa o eliminare l’etichetta da questa spesa.', confirmText: 'Elimina tag', }, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `tariffa aggiornata a ${rate} per il nuovo spazio di lavoro - ${policyName}`, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `tariffa aggiornata a ${rate} per il nuovo workspace - ${policyName}`, conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `tariffa aggiornata a ${rate} per il nuovo workspace del report - ${policyName}`, + `tariffa aggiornata a ${rate} per il nuovo spazio di lavoro della nota spese - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 613fe0b6d801..49399158992e 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1840,9 +1840,9 @@ const translations: TranslationDeepObject = { prompt: 'ワークスペースでタグを有効にすると、この経費の詳細を編集したり、この経費からタグを削除したりできます。', confirmText: 'タグを削除', }, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `新しいワークスペース「${policyName}」の距離レートが${rate}に更新されました`, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `新しいワークスペース「${policyName}」のレートが ${rate} に更新されました`, conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `新しいレポートのワークスペース用のレートを ${rate} に更新しました - ${policyName}`, + `新しいレポートのワークスペース(${policyName})の距離単価を ${rate} に更新しました`, }, transactionMerge: { listPage: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 78df126db0cb..3638580a9fe1 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1854,7 +1854,7 @@ const translations: TranslationDeepObject = { prompt: 'Schakel tags in op de werkruimte om de onkostendetails te bewerken of de tag uit deze onkosten te verwijderen.', confirmText: 'Label verwijderen', }, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `tarief bijgewerkt naar ${rate} voor de nieuwe werkruimte - ${policyName}`, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `tarief bijgewerkt naar ${rate} voor de nieuwe workspace - ${policyName}`, conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => `tarief bijgewerkt naar ${rate} voor de nieuwe rapportwerkruimte - ${policyName}`, }, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 3a0c7d2bcbc3..0b1e5c89aa3e 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1889,9 +1889,9 @@ const translations: TranslationDeepObject = { prompt: 'Włącz tagi w przestrzeni roboczej, aby edytować szczegóły wydatku lub usunąć ten tag z tego wydatku.', confirmText: 'Usuń znacznik', }, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `stawka zaktualizowana na ${rate} dla nowego workspace – ${policyName}`, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `stawka zaktualizowana do ${rate} dla nowego workspace’u – ${policyName}`, conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `stawka została zaktualizowana do ${rate} dla nowej przestrzeni roboczej raportu – ${policyName}`, + `stawka zaktualizowana do ${rate} dla nowej przestrzeni roboczej raportu – ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 6ac2489f0975..50ea006d0c08 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1849,9 +1849,9 @@ const translations: TranslationDeepObject = { confirmText: 'Excluir categoria', }, tagDisabledAlert: {title: 'Tag desativada', prompt: 'Ative as tags no workspace para editar os detalhes da despesa ou excluir a tag desta despesa.', confirmText: 'Excluir tag'}, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `taxa atualizada para ${rate} para o novo espaço de trabalho - ${policyName}`, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `taxa atualizada para ${rate} para o novo workspace - ${policyName}`, conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `taxa atualizada para ${rate} para o novo workspace do relatório - ${policyName}`, + `taxa atualizada para ${rate} para o novo espaço de trabalho do relatório - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 923c78efd4b1..6fc0e9b5d796 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1782,8 +1782,8 @@ const translations: TranslationDeepObject = { deleteConfirmationSomePendingBYOC: '您确定要删除这些报销吗?其中一些处于待处理状态,如果入账后,我们可能会再次导入。', categoryDisabledAlert: {title: '类别已禁用', prompt: '在工作区中启用类别,以编辑报销详情或从此报销中删除该类别。', confirmText: '删除类别'}, tagDisabledAlert: {title: '标签已停用', prompt: '请在工作区中启用标签,以便编辑该报销的详细信息或从此报销中删除该标签。', confirmText: '删除标签'}, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `新工作区(${policyName})的里程费率已更新为 ${rate}`, - conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => `已为新报销单的工作区(${policyName})将里程报销费率更新为 ${rate}`, + conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `新工作区 ${policyName} 的距离费率已更新为 ${rate}`, + conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => `已将新报表工作区(${policyName})的里程费率更新为 ${rate}`, }, transactionMerge: { listPage: { From c182433b6700f9e789ff86e74fc604356fcd3bd0 Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Thu, 10 Sep 2026 04:30:25 +0000 Subject: [PATCH 04/20] Drop the htmlEncode of policyName in the Concierge distance rate message policyName is not allowed to contain HTML characters, so encoding it is unnecessary. This also matches how the other report action helpers interpolate a policyName from originalMessage, such as getWorkspaceDemotedMessage. Removes the test that asserted the encoding behavior. Co-authored-by: dmkt9 --- src/libs/ReportActionsUtils.ts | 6 ++---- tests/unit/ReportActionsUtilsTest.ts | 18 ------------------ 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 2b979b43271d..dd2f87ce9c11 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3427,14 +3427,12 @@ function getConciergeAutoSelectDistanceRateMessage(translate: LocalizedTranslate // The rate is stored in the CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET scale, which is the scale convertAmountToDisplayString divides by, so 67 is displayed as $0.67. const formattedRate = `${convertAmountToDisplayString(rate, currency)} / ${unit}`; - // The workspace name is interpolated into a message that is rendered as HTML, so encode it to prevent a name containing markup from being parsed as HTML. - const encodedPolicyName = Str.htmlEncode(policyName); if (changeType === CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.REPORT_MOVED) { - return translate('iou.conciergeAutoSelectedDistanceRateForMovedReport', {rate: formattedRate, policyName: encodedPolicyName}); + return translate('iou.conciergeAutoSelectedDistanceRateForMovedReport', {rate: formattedRate, policyName}); } - return translate('iou.conciergeAutoSelectedDistanceRate', {rate: formattedRate, policyName: encodedPolicyName}); + return translate('iou.conciergeAutoSelectedDistanceRate', {rate: formattedRate, policyName}); } function getWorkspaceCustomUnitRateDeletedMessage(translate: LocalizedTranslate, action: ReportAction): string { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index b3217e5992f0..9971dbed371f 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -1633,24 +1633,6 @@ describe('ReportActionsUtils', () => { expect(Parser.htmlToText(message)).toBe("rate updated to $0.67 / mi for the new report’s workspace - Hal's Burgers"); }); - it('should escape a workspace name containing markup', () => { - // Given a workspace name containing HTML - const action = buildConciergeAutoSelectDistanceRateAction({ - rate: 67, - currency: 'USD', - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - policyName: 'Hal', - changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.WORKSPACE_CHANGED, - }); - - // When building the message - const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); - - // Then the markup should be encoded rather than rendered - expect(message).toContain('<strong>Hal</strong>'); - expect(message).not.toContain(''); - }); - it('should fall back to the text of the action when the rate details are missing', () => { // Given an action without the rate details const backendText = 'rate updated by the backend'; From 2815d3f29076541acea0f9ccee801aa3cb3ac454 Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Thu, 10 Sep 2026 05:42:14 +0000 Subject: [PATCH 05/20] Encode policyName in the Concierge distance rate message The string this helper returns is HTML: it goes to RenderHTML in the report and is returned as the html fragment of the action, and every plain-text surface (LHN preview, thread title, clipboard) runs it back through Parser.htmlToText. policyName comes off originalMessage, which Auth writes, so it has to be encoded for the HTML sinks. Encoding round-trips cleanly: htmlToText ends in Str.htmlDecode, so the plain-text surfaces still show the name exactly as typed. Without the encode, a name containing
renders as a line break in the report and is turned into a newline in the single-line LHN preview. This reverts c182433, whose parity argument compared this helper against ones that render through plain and so genuinely need no encoding. Co-authored-by: dmkt9 --- src/libs/ReportActionsUtils.ts | 9 +++++++-- tests/unit/ReportActionsUtilsTest.ts | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index dd2f87ce9c11..31b8b5aee50d 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3417,6 +3417,10 @@ function getWorkspaceCustomUnitRateUpdatedMessage(translate: LocalizedTranslate, /** * Builds the Concierge system message explaining that the distance rate of an expense was updated automatically. * It is the single source of the copy for every surface (the report, the LHN preview, copy to clipboard), so the message is localized everywhere. + * + * The returned string is HTML: it is rendered through `RenderHTML` in the report and returned as the `html` fragment of the action, + * and every plain-text surface (LHN preview, thread title, clipboard) runs it back through `Parser.htmlToText`. + * `policyName` is therefore HTML-encoded, so a workspace name is always shown as typed instead of being interpreted as markup. */ function getConciergeAutoSelectDistanceRateMessage(translate: LocalizedTranslate, action: ReportAction): string { const {rate, currency, unit, policyName, changeType} = getOriginalMessage(action as ReportAction) ?? {}; @@ -3427,12 +3431,13 @@ function getConciergeAutoSelectDistanceRateMessage(translate: LocalizedTranslate // The rate is stored in the CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET scale, which is the scale convertAmountToDisplayString divides by, so 67 is displayed as $0.67. const formattedRate = `${convertAmountToDisplayString(rate, currency)} / ${unit}`; + const encodedPolicyName = Str.htmlEncode(policyName); if (changeType === CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.REPORT_MOVED) { - return translate('iou.conciergeAutoSelectedDistanceRateForMovedReport', {rate: formattedRate, policyName}); + return translate('iou.conciergeAutoSelectedDistanceRateForMovedReport', {rate: formattedRate, policyName: encodedPolicyName}); } - return translate('iou.conciergeAutoSelectedDistanceRate', {rate: formattedRate, policyName}); + return translate('iou.conciergeAutoSelectedDistanceRate', {rate: formattedRate, policyName: encodedPolicyName}); } function getWorkspaceCustomUnitRateDeletedMessage(translate: LocalizedTranslate, action: ReportAction): string { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 9971dbed371f..9c2e30920584 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -1676,6 +1676,27 @@ describe('ReportActionsUtils', () => { // Then the action should not be filtered out as an unsupported action type expect(ReportActionsUtils.shouldReportActionBeVisible(action, action.reportActionID, true)).toBe(true); }); + + it('should encode a workspace name that contains markup so it is not rendered as HTML', () => { + // Given a workspace name containing a tag that RenderHTML would render and that htmlToText would turn into a newline + const action = buildConciergeAutoSelectDistanceRateAction({ + rate: 67, + currency: 'USD', + unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + policyName: 'Ops
Team', + changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.WORKSPACE_CHANGED, + }); + + // When building the message + const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); + + // Then the HTML form should carry the name encoded, so RenderHTML shows it as typed instead of breaking the line + expect(message).toContain('Ops<br>Team'); + expect(message).not.toContain('Ops
Team'); + + // And the plain-text surfaces should decode it back to the name as typed, on a single line + expect(Parser.htmlToText(message)).toBe('rate updated to $0.67 / mi for the new workspace - Ops
Team'); + }); }); describe('getReportActionText', () => { From 9d01b51e63c464b10b03383888e78a6a6ea79131 Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Thu, 10 Sep 2026 08:33:01 +0000 Subject: [PATCH 06/20] Match CONCIERGE_AUTO_MATCH_VENDOR: drop the getReportAlternateText branch The custom branch made the LHN render the message with no actor prefix while the search router kept 'Concierge: ', because getChatPreviewParts only suppresses the prefix for actions listed in CUSTOM_ALTERNATE_TEXT_ACTION_NAMES. Removing the branch lets both surfaces fall through to the generic '${displayName}: ' handling, which is what CONCIERGE_AUTO_MATCH_VENDOR already does. The preview stays localized: the LHN passes lastMessageTextFromReport as undefined, so getReportAlternateText derives it from getLastMessageTextForReport, which still translates the copy. Co-authored-by: dmkt9 --- src/libs/ReportAlternateTextUtils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/ReportAlternateTextUtils.ts b/src/libs/ReportAlternateTextUtils.ts index d637c38874a6..3c35ffeecbc0 100644 --- a/src/libs/ReportAlternateTextUtils.ts +++ b/src/libs/ReportAlternateTextUtils.ts @@ -1489,8 +1489,6 @@ function getReportAlternateText({ alternateText = translate('iou.reopened'); } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.TRAVEL_UPDATE)) { alternateText = getTravelUpdateMessage(translate, lastAction); - } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE)) { - alternateText = Parser.htmlToText(getConciergeAutoSelectDistanceRateMessage(translate, lastAction)); } else if ( isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL) || isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.REROUTE) || From 0c2ad353e66237273e87f1542a749cc2449d19f6 Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Thu, 10 Sep 2026 09:32:28 +0000 Subject: [PATCH 07/20] Localize the distance unit in the Concierge auto-select rate message The raw 'mi' / 'km' unit key was concatenated into the copy, so it stayed in English in every locale. Map it to common.mile / common.kilometer the same way getWorkspaceCustomUnitRateAddedMessage already does. Co-authored-by: dmkt9 --- src/libs/ReportActionsUtils.ts | 4 +++- tests/unit/ReportActionsUtilsTest.ts | 23 +++++++++++++++++++--- tests/unit/ReportAlternateTextUtilsTest.ts | 2 +- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 31b8b5aee50d..58defd4a70f4 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3429,8 +3429,10 @@ function getConciergeAutoSelectDistanceRateMessage(translate: LocalizedTranslate return getReportActionText(action); } + // The raw unit is the untranslatable 'mi' / 'km' key, so it is mapped to a translated label the same way getWorkspaceCustomUnitRateAddedMessage does. + const unitLabel = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); // The rate is stored in the CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET scale, which is the scale convertAmountToDisplayString divides by, so 67 is displayed as $0.67. - const formattedRate = `${convertAmountToDisplayString(rate, currency)} / ${unit}`; + const formattedRate = `${convertAmountToDisplayString(rate, currency)} / ${unitLabel}`; const encodedPolicyName = Str.htmlEncode(policyName); if (changeType === CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.REPORT_MOVED) { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 9c2e30920584..e560de663b0b 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -1613,7 +1613,24 @@ describe('ReportActionsUtils', () => { const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); // Then it should name the new workspace and the formatted rate - expect(Parser.htmlToText(message)).toBe("rate updated to $0.67 / mi for the new workspace - Hal's Burgers"); + expect(Parser.htmlToText(message)).toBe("rate updated to $0.67 / mile for the new workspace - Hal's Burgers"); + }); + + it('should translate the distance unit instead of showing the raw unit key', () => { + // Given an action whose rate is expressed in kilometers + const action = buildConciergeAutoSelectDistanceRateAction({ + rate: 67, + currency: 'USD', + unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS, + policyName: "Hal's Burgers", + changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.WORKSPACE_CHANGED, + }); + + // When building the message + const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); + + // Then it should use the translated unit label rather than the raw 'km' key + expect(Parser.htmlToText(message)).toBe("rate updated to $0.67 / kilometer for the new workspace - Hal's Burgers"); }); it('should describe a report that moved to another workspace', () => { @@ -1630,7 +1647,7 @@ describe('ReportActionsUtils', () => { const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); // Then it should say the workspace of the report changed - expect(Parser.htmlToText(message)).toBe("rate updated to $0.67 / mi for the new report’s workspace - Hal's Burgers"); + expect(Parser.htmlToText(message)).toBe("rate updated to $0.67 / mile for the new report’s workspace - Hal's Burgers"); }); it('should fall back to the text of the action when the rate details are missing', () => { @@ -1695,7 +1712,7 @@ describe('ReportActionsUtils', () => { expect(message).not.toContain('Ops
Team'); // And the plain-text surfaces should decode it back to the name as typed, on a single line - expect(Parser.htmlToText(message)).toBe('rate updated to $0.67 / mi for the new workspace - Ops
Team'); + expect(Parser.htmlToText(message)).toBe('rate updated to $0.67 / mile for the new workspace - Ops
Team'); }); }); diff --git a/tests/unit/ReportAlternateTextUtilsTest.ts b/tests/unit/ReportAlternateTextUtilsTest.ts index 96d4f5f02556..33be9a72734c 100644 --- a/tests/unit/ReportAlternateTextUtilsTest.ts +++ b/tests/unit/ReportAlternateTextUtilsTest.ts @@ -1312,7 +1312,7 @@ describe('ReportAlternateTextUtils', () => { }); // Then it should be built from the translation rather than the text the backend provided - expect(lastMessage).toBe("rate updated to $0.67 / mi for the new workspace - Hal's Burgers"); + expect(lastMessage).toBe("rate updated to $0.67 / mile for the new workspace - Hal's Burgers"); }); it('ADD_AGENT_RULE action', async () => { const report: Report = createRandomReport(0, undefined); From e1cc6b2660acb953d112d177bfe043571f4da2e0 Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Thu, 10 Sep 2026 09:47:56 +0000 Subject: [PATCH 08/20] Treat the Concierge auto-select rate message as plain text policyName cannot contain HTML markup, so the message no longer needs to be HTML-encoded on the way in or decoded on the way out. This matches the TRAVEL_UPDATE helper this action was modeled on. Co-authored-by: dmkt9 --- src/libs/ReportActionsUtils.ts | 7 +++---- src/libs/ReportAlternateTextUtils.ts | 2 +- src/libs/ReportNameUtils.ts | 2 +- tests/unit/ReportActionsUtilsTest.ts | 30 ++++------------------------ 4 files changed, 9 insertions(+), 32 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 58defd4a70f4..f6e31cfc9447 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -2510,7 +2510,7 @@ function getReportActionMessageFragments(translate: LocalizedTranslate, action: if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE)) { const message = getConciergeAutoSelectDistanceRateMessage(translate, action); - return [{text: Parser.htmlToText(message), html: `${message}`, type: 'COMMENT'}]; + return [{text: message, html: `${message}`, type: 'COMMENT'}]; } if (isDynamicExternalWorkflowSubmitFailedAction(action)) { @@ -3433,13 +3433,12 @@ function getConciergeAutoSelectDistanceRateMessage(translate: LocalizedTranslate const unitLabel = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); // The rate is stored in the CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET scale, which is the scale convertAmountToDisplayString divides by, so 67 is displayed as $0.67. const formattedRate = `${convertAmountToDisplayString(rate, currency)} / ${unitLabel}`; - const encodedPolicyName = Str.htmlEncode(policyName); if (changeType === CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.REPORT_MOVED) { - return translate('iou.conciergeAutoSelectedDistanceRateForMovedReport', {rate: formattedRate, policyName: encodedPolicyName}); + return translate('iou.conciergeAutoSelectedDistanceRateForMovedReport', {rate: formattedRate, policyName}); } - return translate('iou.conciergeAutoSelectedDistanceRate', {rate: formattedRate, policyName: encodedPolicyName}); + return translate('iou.conciergeAutoSelectedDistanceRate', {rate: formattedRate, policyName}); } function getWorkspaceCustomUnitRateDeletedMessage(translate: LocalizedTranslate, action: ReportAction): string { diff --git a/src/libs/ReportAlternateTextUtils.ts b/src/libs/ReportAlternateTextUtils.ts index 3c35ffeecbc0..78d9177812a0 100644 --- a/src/libs/ReportAlternateTextUtils.ts +++ b/src/libs/ReportAlternateTextUtils.ts @@ -759,7 +759,7 @@ function getLastMessageTextForReport({ } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.DYNAMIC_EXTERNAL_WORKFLOW_ROUTED)) { lastMessageTextFromReport = getDynamicExternalWorkflowRoutedMessage(lastReportAction, translate); } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE)) { - lastMessageTextFromReport = Parser.htmlToText(getConciergeAutoSelectDistanceRateMessage(translate, lastReportAction)); + lastMessageTextFromReport = getConciergeAutoSelectDistanceRateMessage(translate, lastReportAction); } if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT)) { lastMessageTextFromReport = getPolicyChangeLogMaxExpenseAmountMessage(translate, lastReportAction, convertToDisplayString); diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index 06b4385324ac..34c7c2106ca2 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -875,7 +875,7 @@ function computeReportNameBasedOnReportAction({ } if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE)) { - return Parser.htmlToText(getConciergeAutoSelectDistanceRateMessage(translate, parentReportAction)); + return getConciergeAutoSelectDistanceRateMessage(translate, parentReportAction); } if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CUSTOM_UNIT_RATE)) { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index e560de663b0b..163cb69c8608 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -4,7 +4,6 @@ import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import getReportURLForCurrentContext from '@libs/Navigation/helpers/getReportURLForCurrentContext'; import {setHasRadio} from '@libs/NetworkState'; -import Parser from '@libs/Parser'; import {isExpenseReport} from '@libs/ReportUtils'; import IntlStore from '@src/languages/IntlStore'; @@ -1613,7 +1612,7 @@ describe('ReportActionsUtils', () => { const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); // Then it should name the new workspace and the formatted rate - expect(Parser.htmlToText(message)).toBe("rate updated to $0.67 / mile for the new workspace - Hal's Burgers"); + expect(message).toBe("rate updated to $0.67 / mile for the new workspace - Hal's Burgers"); }); it('should translate the distance unit instead of showing the raw unit key', () => { @@ -1630,7 +1629,7 @@ describe('ReportActionsUtils', () => { const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); // Then it should use the translated unit label rather than the raw 'km' key - expect(Parser.htmlToText(message)).toBe("rate updated to $0.67 / kilometer for the new workspace - Hal's Burgers"); + expect(message).toBe("rate updated to $0.67 / kilometer for the new workspace - Hal's Burgers"); }); it('should describe a report that moved to another workspace', () => { @@ -1647,7 +1646,7 @@ describe('ReportActionsUtils', () => { const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); // Then it should say the workspace of the report changed - expect(Parser.htmlToText(message)).toBe("rate updated to $0.67 / mile for the new report’s workspace - Hal's Burgers"); + expect(message).toBe("rate updated to $0.67 / mile for the new report’s workspace - Hal's Burgers"); }); it('should fall back to the text of the action when the rate details are missing', () => { @@ -1677,7 +1676,7 @@ describe('ReportActionsUtils', () => { // Then they should be built from the same message const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); - expect(fragments).toEqual([{text: Parser.htmlToText(message), html: `${message}`, type: 'COMMENT'}]); + expect(fragments).toEqual([{text: message, html: `${message}`, type: 'COMMENT'}]); }); it('should be visible in the report', () => { @@ -1693,27 +1692,6 @@ describe('ReportActionsUtils', () => { // Then the action should not be filtered out as an unsupported action type expect(ReportActionsUtils.shouldReportActionBeVisible(action, action.reportActionID, true)).toBe(true); }); - - it('should encode a workspace name that contains markup so it is not rendered as HTML', () => { - // Given a workspace name containing a tag that RenderHTML would render and that htmlToText would turn into a newline - const action = buildConciergeAutoSelectDistanceRateAction({ - rate: 67, - currency: 'USD', - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - policyName: 'Ops
Team', - changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.WORKSPACE_CHANGED, - }); - - // When building the message - const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); - - // Then the HTML form should carry the name encoded, so RenderHTML shows it as typed instead of breaking the line - expect(message).toContain('Ops<br>Team'); - expect(message).not.toContain('Ops
Team'); - - // And the plain-text surfaces should decode it back to the name as typed, on a single line - expect(Parser.htmlToText(message)).toBe('rate updated to $0.67 / mile for the new workspace - Ops
Team'); - }); }); describe('getReportActionText', () => { From 51af48321c0b3380180a18639810e27f6f44c6f5 Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Thu, 10 Sep 2026 09:59:15 +0000 Subject: [PATCH 09/20] Revert the distance unit localization in the Concierge rate message Reverts 0c2ad35. The unit goes back to the raw 'mi' / 'km' value so the rendered copy matches the issue spec ($0.67 / mi) instead of the full word the common.mile / common.kilometer keys produce. Co-authored-by: dmkt9 --- src/libs/ReportActionsUtils.ts | 4 +--- tests/unit/ReportActionsUtilsTest.ts | 21 ++------------------- tests/unit/ReportAlternateTextUtilsTest.ts | 2 +- 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f6e31cfc9447..46a28fe12961 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3429,10 +3429,8 @@ function getConciergeAutoSelectDistanceRateMessage(translate: LocalizedTranslate return getReportActionText(action); } - // The raw unit is the untranslatable 'mi' / 'km' key, so it is mapped to a translated label the same way getWorkspaceCustomUnitRateAddedMessage does. - const unitLabel = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); // The rate is stored in the CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET scale, which is the scale convertAmountToDisplayString divides by, so 67 is displayed as $0.67. - const formattedRate = `${convertAmountToDisplayString(rate, currency)} / ${unitLabel}`; + const formattedRate = `${convertAmountToDisplayString(rate, currency)} / ${unit}`; if (changeType === CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.REPORT_MOVED) { return translate('iou.conciergeAutoSelectedDistanceRateForMovedReport', {rate: formattedRate, policyName}); diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 163cb69c8608..9411b62e2943 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -1612,24 +1612,7 @@ describe('ReportActionsUtils', () => { const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); // Then it should name the new workspace and the formatted rate - expect(message).toBe("rate updated to $0.67 / mile for the new workspace - Hal's Burgers"); - }); - - it('should translate the distance unit instead of showing the raw unit key', () => { - // Given an action whose rate is expressed in kilometers - const action = buildConciergeAutoSelectDistanceRateAction({ - rate: 67, - currency: 'USD', - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS, - policyName: "Hal's Burgers", - changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.WORKSPACE_CHANGED, - }); - - // When building the message - const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); - - // Then it should use the translated unit label rather than the raw 'km' key - expect(message).toBe("rate updated to $0.67 / kilometer for the new workspace - Hal's Burgers"); + expect(message).toBe("rate updated to $0.67 / mi for the new workspace - Hal's Burgers"); }); it('should describe a report that moved to another workspace', () => { @@ -1646,7 +1629,7 @@ describe('ReportActionsUtils', () => { const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); // Then it should say the workspace of the report changed - expect(message).toBe("rate updated to $0.67 / mile for the new report’s workspace - Hal's Burgers"); + expect(message).toBe("rate updated to $0.67 / mi for the new report’s workspace - Hal's Burgers"); }); it('should fall back to the text of the action when the rate details are missing', () => { diff --git a/tests/unit/ReportAlternateTextUtilsTest.ts b/tests/unit/ReportAlternateTextUtilsTest.ts index 33be9a72734c..96d4f5f02556 100644 --- a/tests/unit/ReportAlternateTextUtilsTest.ts +++ b/tests/unit/ReportAlternateTextUtilsTest.ts @@ -1312,7 +1312,7 @@ describe('ReportAlternateTextUtils', () => { }); // Then it should be built from the translation rather than the text the backend provided - expect(lastMessage).toBe("rate updated to $0.67 / mile for the new workspace - Hal's Burgers"); + expect(lastMessage).toBe("rate updated to $0.67 / mi for the new workspace - Hal's Burgers"); }); it('ADD_AGENT_RULE action', async () => { const report: Report = createRandomReport(0, undefined); From a553870faece65dd0b078b4cf104673e569a9b99 Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Thu, 10 Sep 2026 10:12:40 +0000 Subject: [PATCH 10/20] Correct the stale HTML contract on the Concierge rate message helper Co-authored-by: dmkt9 --- src/libs/ReportActionsUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 46a28fe12961..1910b619905f 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3416,11 +3416,11 @@ function getWorkspaceCustomUnitRateUpdatedMessage(translate: LocalizedTranslate, /** * Builds the Concierge system message explaining that the distance rate of an expense was updated automatically. - * It is the single source of the copy for every surface (the report, the LHN preview, copy to clipboard), so the message is localized everywhere. + * It is the single source of the copy for every surface (the report, the LHN preview, the thread title, copy to clipboard), so the message is localized everywhere. * - * The returned string is HTML: it is rendered through `RenderHTML` in the report and returned as the `html` fragment of the action, - * and every plain-text surface (LHN preview, thread title, clipboard) runs it back through `Parser.htmlToText`. - * `policyName` is therefore HTML-encoded, so a workspace name is always shown as typed instead of being interpreted as markup. + * The returned string is plain text and `policyName` is interpolated as typed, so it is not safe to hand to `RenderHTML` on its own. + * The LHN preview, thread title, and clipboard consume it directly; the report body and the action's `html` fragment wrap it in ``, + * which is what `getTravelUpdateMessage` and `getDynamicExternalWorkflowRoutedMessage` do with their own plain-text output. */ function getConciergeAutoSelectDistanceRateMessage(translate: LocalizedTranslate, action: ReportAction): string { const {rate, currency, unit, policyName, changeType} = getOriginalMessage(action as ReportAction) ?? {}; From 9bebf5103092c9db46cbeea0a043308d3c494065 Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Thu, 10 Sep 2026 10:31:01 +0000 Subject: [PATCH 11/20] Trim the Concierge rate message helper docstring to a one-line summary Co-authored-by: dmkt9 --- src/libs/ReportActionsUtils.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 1910b619905f..79160f898a1a 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3416,11 +3416,6 @@ function getWorkspaceCustomUnitRateUpdatedMessage(translate: LocalizedTranslate, /** * Builds the Concierge system message explaining that the distance rate of an expense was updated automatically. - * It is the single source of the copy for every surface (the report, the LHN preview, the thread title, copy to clipboard), so the message is localized everywhere. - * - * The returned string is plain text and `policyName` is interpolated as typed, so it is not safe to hand to `RenderHTML` on its own. - * The LHN preview, thread title, and clipboard consume it directly; the report body and the action's `html` fragment wrap it in ``, - * which is what `getTravelUpdateMessage` and `getDynamicExternalWorkflowRoutedMessage` do with their own plain-text output. */ function getConciergeAutoSelectDistanceRateMessage(translate: LocalizedTranslate, action: ReportAction): string { const {rate, currency, unit, policyName, changeType} = getOriginalMessage(action as ReportAction) ?? {}; From d51765ccb8d0121f51f51ccc16ddd6ea489d7173 Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Fri, 11 Sep 2026 17:02:44 +0000 Subject: [PATCH 12/20] Describe the automatic rate change per expense instead of naming one rate on the report A report can hold many distance expenses, each landing on a different rate when its workspace changes, so the Concierge action on the report can't name a single rate. It now names only the destination workspace; the per-expense rate changes are described by a MODIFIED_EXPENSE action on each transaction thread. That also drops the reportMoved variant, because moving one expense between reports no longer posts this action on either report. Also fixes the MODIFIED_EXPENSE message calling a rate change a distance change. The merchant-format regex it gated on asserted the number formatting, which is localized, so it rejected valid merchants in every locale that writes decimals with a comma. Comparing the two halves structurally instead also handles the destination workspace using the other distance unit. Co-authored-by: dmkt9 --- src/CONST/index.ts | 5 -- src/languages/de.ts | 4 +- src/languages/el.ts | 4 +- src/languages/en.ts | 6 +- src/languages/es.ts | 4 +- src/languages/fr.ts | 4 +- src/languages/it.ts | 4 +- src/languages/ja.ts | 4 +- src/languages/nl.ts | 4 +- src/languages/pl.ts | 4 +- src/languages/pt-BR.ts | 4 +- src/languages/zh-hans.ts | 3 +- src/libs/ModifiedExpenseMessage.ts | 35 +++++++-- src/libs/ReportActionsUtils.ts | 15 +--- src/types/onyx/OriginalMessage.ts | 22 ++---- tests/unit/ModifiedExpenseMessageTest.ts | 87 ++++++++++++++++++++++ tests/unit/ReportActionsUtilsTest.ts | 54 ++++---------- tests/unit/ReportAlternateTextUtilsTest.ts | 6 +- 18 files changed, 153 insertions(+), 116 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 4beeec0002ab..e38f1f4f2729 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1940,10 +1940,6 @@ const CONST = { ACCEPT: 'accept', DECLINE: 'decline', }, - CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE: { - WORKSPACE_CHANGED: 'workspaceChanged', - REPORT_MOVED: 'reportMoved', - }, ARCHIVE_REASON: { DEFAULT: 'default', ACCOUNT_CLOSED: 'accountClosed', @@ -5493,7 +5489,6 @@ const CONST = { OTHER_INVISIBLE_CHARACTERS: /[\u3164\u115f\u1160\uffa0\u2800]/g, SHORT_MENTION_HTML: /(.*?)<\/mention-short>/g, REPORT_ID_FROM_PATH: /(? = { prompt: 'Aktivieren Sie Tags im Workspace, um die Ausgabendetails zu bearbeiten oder den Tag aus dieser Ausgabe zu löschen.', confirmText: 'Tag löschen', }, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `Satz auf ${rate} für den neuen Workspace „${policyName}“ aktualisiert`, - conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `Satz pro Einheit auf ${rate} für den neuen Bericht-Arbeitsbereich aktualisiert – ${policyName}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `Entfernungssätze für den neuen Workspace „${policyName}“ aktualisiert`, }, transactionMerge: { listPage: { diff --git a/src/languages/el.ts b/src/languages/el.ts index 8682829cd665..27c982afe769 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -1912,9 +1912,7 @@ const translations: TranslationDeepObject = { whatIsHoldExplainDM: 'Η αναμονή είναι σαν να πατάτε «παύση» σε μία δαπάνη μέχρι να είστε έτοιμοι να τη στείλετε.', holdIsLeftBehindDM: 'Οι δεσμευμένες δαπάνες δεν θα αποσταλούν μέχρι να καταργήσετε τη δέσμευση.', unholdWhenReadyDM: 'Αποδεσμεύστε τις δαπάνες όταν είστε έτοιμοι να τις στείλετε.', - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `ο συντελεστής ενημερώθηκε σε ${rate} για τον νέο χώρο εργασίας - ${policyName}`, - conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `ο συντελεστής ενημερώθηκε σε ${rate} για τον νέο χώρο εργασίας της έκθεσης - ${policyName}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `οι συντελεστές απόστασης ενημερώθηκαν για τον νέο χώρο εργασίας - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 2226f9901888..bc42f80a9290 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1908,10 +1908,8 @@ const translations = { correctRateError: 'Fix the rate error and try again.', AskToExplain: `. Explain`, conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge matched this expense to ${vendorName}`, - // @context "rate" is the distance rate of an expense (an amount of money per mile or kilometer), not a price, a fee or a rating. "rate" is lowercase because the sentence continues a system message. - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `rate updated to ${rate} for the new workspace - ${policyName}`, - // @context "rate" is the distance rate of an expense (an amount of money per mile or kilometer), not a price, a fee or a rating. Shown when the expense report was moved to another workspace, so the workspace of the report is the one that changed. - conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => `rate updated to ${rate} for the new report’s workspace - ${policyName}`, + // @context "distance rates" are the per-mile or per-kilometer amounts a workspace reimburses for mileage, not prices, fees or ratings. Lowercase because the sentence continues a system message. + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `distance rates updated for the new workspace - ${policyName}`, rulesModifiedFields: { reimbursable: (value: boolean) => (value ? 'marked the expense as "reimbursable"' : 'marked the expense as "non-reimbursable"'), billable: (value: boolean) => (value ? 'marked the expense as "billable"' : 'marked the expense as "non-billable"'), diff --git a/src/languages/es.ts b/src/languages/es.ts index 9f93666a842e..62c290083bb6 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1853,9 +1853,7 @@ const translations: TranslationDeepObject = { prompt: 'Habilita las etiquetas en el espacio de trabajo para editar los detalles del gasto o eliminar la etiqueta de este gasto.', confirmText: 'Eliminar etiqueta', }, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `se actualizó la tasa a ${rate} para el nuevo espacio de trabajo - ${policyName}`, - conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `tasa actualizada a ${rate} para el nuevo espacio de trabajo del informe - ${policyName}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `se actualizaron las tasas de distancia para el nuevo espacio de trabajo - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index be952eb9a508..a9bdfd369e04 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1868,9 +1868,7 @@ const translations: TranslationDeepObject = { prompt: 'Active les tags dans l’espace de travail pour modifier les détails de la dépense ou supprimer le tag de cette dépense.', confirmText: 'Supprimer le tag', }, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `taux mis à jour à ${rate} pour le nouvel espace de travail - ${policyName}`, - conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `taux mis à jour à ${rate} pour le nouvel espace de travail de la note de frais - ${policyName}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `taux kilométriques mis à jour pour le nouvel espace de travail - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 5516466e6d62..fefab3f0880b 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1858,9 +1858,7 @@ const translations: TranslationDeepObject = { prompt: 'Abilita le etichette nello spazio di lavoro per modificare i dettagli della spesa o eliminare l’etichetta da questa spesa.', confirmText: 'Elimina tag', }, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `tariffa aggiornata a ${rate} per il nuovo workspace - ${policyName}`, - conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `tariffa aggiornata a ${rate} per il nuovo spazio di lavoro della nota spese - ${policyName}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `tariffe di distanza aggiornate per il nuovo workspace - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 49399158992e..8404d57b0147 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1840,9 +1840,7 @@ const translations: TranslationDeepObject = { prompt: 'ワークスペースでタグを有効にすると、この経費の詳細を編集したり、この経費からタグを削除したりできます。', confirmText: 'タグを削除', }, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `新しいワークスペース「${policyName}」のレートが ${rate} に更新されました`, - conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `新しいレポートのワークスペース(${policyName})の距離単価を ${rate} に更新しました`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `新しいワークスペース「${policyName}」の距離レートが更新されました`, }, transactionMerge: { listPage: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 3638580a9fe1..2e5cab65b7ca 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1854,9 +1854,7 @@ const translations: TranslationDeepObject = { prompt: 'Schakel tags in op de werkruimte om de onkostendetails te bewerken of de tag uit deze onkosten te verwijderen.', confirmText: 'Label verwijderen', }, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `tarief bijgewerkt naar ${rate} voor de nieuwe workspace - ${policyName}`, - conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `tarief bijgewerkt naar ${rate} voor de nieuwe rapportwerkruimte - ${policyName}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `afstandstarieven bijgewerkt voor de nieuwe workspace - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 0b1e5c89aa3e..63d265cff6cf 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1889,9 +1889,7 @@ const translations: TranslationDeepObject = { prompt: 'Włącz tagi w przestrzeni roboczej, aby edytować szczegóły wydatku lub usunąć ten tag z tego wydatku.', confirmText: 'Usuń znacznik', }, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `stawka zaktualizowana do ${rate} dla nowego workspace’u – ${policyName}`, - conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `stawka zaktualizowana do ${rate} dla nowej przestrzeni roboczej raportu – ${policyName}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `stawki za odległość zaktualizowane dla nowego workspace’u – ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 50ea006d0c08..5baf8003ec70 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1849,9 +1849,7 @@ const translations: TranslationDeepObject = { confirmText: 'Excluir categoria', }, tagDisabledAlert: {title: 'Tag desativada', prompt: 'Ative as tags no workspace para editar os detalhes da despesa ou excluir a tag desta despesa.', confirmText: 'Excluir tag'}, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `taxa atualizada para ${rate} para o novo workspace - ${policyName}`, - conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => - `taxa atualizada para ${rate} para o novo espaço de trabalho do relatório - ${policyName}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `taxas de distância atualizadas para o novo workspace - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 6fc0e9b5d796..b84c53b90bcd 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1782,8 +1782,7 @@ const translations: TranslationDeepObject = { deleteConfirmationSomePendingBYOC: '您确定要删除这些报销吗?其中一些处于待处理状态,如果入账后,我们可能会再次导入。', categoryDisabledAlert: {title: '类别已禁用', prompt: '在工作区中启用类别,以编辑报销详情或从此报销中删除该类别。', confirmText: '删除类别'}, tagDisabledAlert: {title: '标签已停用', prompt: '请在工作区中启用标签,以便编辑该报销的详细信息或从此报销中删除该标签。', confirmText: '删除标签'}, - conciergeAutoSelectedDistanceRate: ({rate, policyName}: {rate: string; policyName: string}) => `新工作区 ${policyName} 的距离费率已更新为 ${rate}`, - conciergeAutoSelectedDistanceRateForMovedReport: ({rate, policyName}: {rate: string; policyName: string}) => `已将新报表工作区(${policyName})的里程费率更新为 ${rate}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `新工作区 ${policyName} 的距离费率已更新`, }, transactionMerge: { listPage: { diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 576ab794ec80..73327cf20f0c 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -128,18 +128,37 @@ function getMessageLine(translate: LocalizedTranslate, prefix: string, messageFr }, prefix); } +/** + * Splits the distance half of a distance merchant ("10.00 mi") into its quantity and its unit. The quantity is only comparable within one unit, because the same journey + * reads as a different number in miles and in kilometers. + */ +function splitDistanceAndUnit(distance: string): [quantity: string, unit: string] { + const separatorIndex = distance.lastIndexOf(' '); + if (separatorIndex === -1) { + return [distance, '']; + } + return [distance.slice(0, separatorIndex), distance.slice(separatorIndex + 1)]; +} + function getForDistanceRequest(translate: LocalizedTranslate, newMerchant: string, oldMerchant: string, newAmount: string, oldAmount: string): string { let changedField: 'distance' | 'rate' = 'distance'; - if (CONST.REGEX.DISTANCE_MERCHANT.test(newMerchant) && CONST.REGEX.DISTANCE_MERCHANT.test(oldMerchant)) { - const oldValues = oldMerchant.split('@'); - const oldDistance = oldValues.at(0)?.trim() ?? ''; - const oldRate = oldValues.at(1)?.trim() ?? ''; - const newValues = newMerchant.split('@'); - const newDistance = newValues.at(0)?.trim() ?? ''; - const newRate = newValues.at(1)?.trim() ?? ''; + const oldValues = oldMerchant.split(CONST.DISTANCE_MERCHANT_SEPARATOR).map((value) => value.trim()); + const newValues = newMerchant.split(CONST.DISTANCE_MERCHANT_SEPARATOR).map((value) => value.trim()); + + // Both merchants have to be " @ " for either half to be comparable. This is a structural check rather than a format one on purpose: matching the rendered + // merchant against a regex also asserts the number formatting, which is localized, so it rejected valid merchants in every locale that writes decimals with a comma. + if (oldValues.length === 2 && newValues.length === 2 && oldValues.every(Boolean) && newValues.every(Boolean)) { + const [oldDistance, oldUnit] = splitDistanceAndUnit(oldValues.at(0) ?? ''); + const [newDistance, newUnit] = splitDistanceAndUnit(newValues.at(0) ?? ''); + const oldRate = oldValues.at(1); + const newRate = newValues.at(1); + + // A changed unit means the journey was re-expressed, not re-measured, so the distance itself is unchanged even though the number is. That is what happens when an + // expense lands on a workspace whose rates are in the other unit, and it used to be reported as a distance change. + const didDistanceChange = oldUnit === newUnit && oldDistance !== newDistance; - if (oldDistance === newDistance && oldRate !== newRate) { + if (!didDistanceChange && oldRate !== newRate) { changedField = 'rate'; } } else { diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 79160f898a1a..473e024563e5 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3415,23 +3415,16 @@ function getWorkspaceCustomUnitRateUpdatedMessage(translate: LocalizedTranslate, } /** - * Builds the Concierge system message explaining that the distance rate of an expense was updated automatically. + * Builds the Concierge system message explaining that the distance rates of a report's expenses were re-selected automatically. */ function getConciergeAutoSelectDistanceRateMessage(translate: LocalizedTranslate, action: ReportAction): string { - const {rate, currency, unit, policyName, changeType} = getOriginalMessage(action as ReportAction) ?? {}; + const {policyName} = getOriginalMessage(action as ReportAction) ?? {}; - if (!rate || !currency || !unit || !policyName) { + if (!policyName) { return getReportActionText(action); } - // The rate is stored in the CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET scale, which is the scale convertAmountToDisplayString divides by, so 67 is displayed as $0.67. - const formattedRate = `${convertAmountToDisplayString(rate, currency)} / ${unit}`; - - if (changeType === CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.REPORT_MOVED) { - return translate('iou.conciergeAutoSelectedDistanceRateForMovedReport', {rate: formattedRate, policyName}); - } - - return translate('iou.conciergeAutoSelectedDistanceRate', {rate: formattedRate, policyName}); + return translate('iou.conciergeAutoSelectedDistanceRates', {policyName}); } function getWorkspaceCustomUnitRateDeletedMessage(translate: LocalizedTranslate, action: ReportAction): string { diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index c7fed657173e..bc4f82a8a39b 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -7,7 +7,7 @@ import type {CardID} from './Card'; import type {PolicyRuleTaxRate} from './ExpenseRule'; import type {Attendee} from './IOU'; import type {OldDotOriginalMessageMap} from './OldDotAction'; -import type {AllConnectionName, Unit} from './Policy'; +import type {AllConnectionName} from './Policy'; import type {PolicyChangeLogCopyReportActionNames} from './ReportAction'; import type ReportActionName from './ReportActionName'; import type {Reservation, TransactionCommentVendor} from './Transaction'; @@ -955,22 +955,14 @@ type OriginalMessageConciergeAutoMatchVendor = { reasoning?: string; }; -/** Model of `concierge auto select distance rate` report action — emitted when the distance rate of an expense is changed automatically because its workspace changed. */ +/** + * Model of `concierge auto select distance rate` report action — posted on an expense report when the report's workspace changes and the distance rates of its expenses are + * re-selected automatically. The individual rate changes are described by a `MODIFIED_EXPENSE` action on each expense's transaction thread, so this action names no rate itself: + * one report can hold many distance expenses, and each can end up on a different rate. + */ type OriginalMessageConciergeAutoSelectDistanceRate = { - /** The new rate, in the `CONST.POLICY.CUSTOM_UNIT_RATE_BASE_OFFSET` scale (e.g. 67 renders as $0.67) */ - rate?: number; - - /** Currency of the new rate */ - currency?: string; - - /** Distance unit of the new rate */ - unit?: Unit; - - /** Name of the workspace the new rate belongs to */ + /** Name of the workspace the report was moved to, whose rates were applied */ policyName?: string; - - /** Whether the workspace of the expense changed directly, or the report the expense belongs to was moved to another workspace */ - changeType?: ValueOf; }; /** Policy rules modified fields. Each member holds the new value the rule wrote, not the current one */ diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index 5de3d626f9a2..2821c9b10d68 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -957,6 +957,93 @@ describe('ModifiedExpenseMessage', () => { }); }); + describe('when the distance rate is changed and the new rate is in another unit', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + oldMerchant: '10.00 mi @ $0.50 / mi', + merchant: '16.09 km @ $0.67 / km', + oldAmount: 500, + amount: 1078, + oldCurrency: CONST.CURRENCY.USD, + currency: CONST.CURRENCY.USD, + }, + }; + + it('then the message still says the rate is changed, because the journey was re-expressed rather than re-measured', () => { + const expectedResult = `changed the rate to ${reportAction.originalMessage.merchant} (previously ${reportAction.originalMessage.oldMerchant}), which updated the amount to $10.78 (previously $5.00)`; + const result = getForReportAction({ + convertToDisplayString, + translate: translateLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + currentUserLogin: CURRENT_USER_LOGIN, + }); + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the distance rate is changed and the rate is formatted with a decimal comma', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + oldMerchant: '56.36 mi @ $0,70 / mi', + merchant: '56.36 mi @ $0,99 / mi', + oldAmount: 3945, + amount: 5580, + oldCurrency: CONST.CURRENCY.USD, + currency: CONST.CURRENCY.USD, + }, + }; + + it('then the message says the rate is changed, because the comparison does not assume a decimal point', () => { + const expectedResult = `changed the rate to ${reportAction.originalMessage.merchant} (previously ${reportAction.originalMessage.oldMerchant}), which updated the amount to $55.80 (previously $39.45)`; + const result = getForReportAction({ + convertToDisplayString, + translate: translateLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + currentUserLogin: CURRENT_USER_LOGIN, + }); + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the old merchant is not a distance merchant', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + oldMerchant: 'Pending...', + merchant: '56.36 mi @ $0.99 / mi', + oldAmount: 3945, + amount: 5580, + oldCurrency: CONST.CURRENCY.USD, + currency: CONST.CURRENCY.USD, + }, + }; + + it('then the message falls back to describing a distance change, because neither half is comparable', () => { + const expectedResult = `changed the distance to ${reportAction.originalMessage.merchant} (previously ${reportAction.originalMessage.oldMerchant}), which updated the amount to $55.80 (previously $39.45)`; + const result = getForReportAction({ + convertToDisplayString, + translate: translateLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + currentUserLogin: CURRENT_USER_LOGIN, + }); + expect(result).toEqual(expectedResult); + }); + }); + describe('when moving an expense', () => { it('returns the movedFromOrToReportMessage message when provided', () => { const reportAction = { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 9411b62e2943..3fa589e8e494 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -1598,44 +1598,32 @@ describe('ReportActionsUtils', () => { }; } - it('should describe a direct workspace change', () => { - // Given an action whose workspace changed directly - const action = buildConciergeAutoSelectDistanceRateAction({ - rate: 67, - currency: 'USD', - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - policyName: "Hal's Burgers", - changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.WORKSPACE_CHANGED, - }); + it('should name the workspace the report moved to', () => { + // Given an action for a report whose workspace changed + const action = buildConciergeAutoSelectDistanceRateAction({policyName: "Hal's Burgers"}); // When building the message const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); - // Then it should name the new workspace and the formatted rate - expect(message).toBe("rate updated to $0.67 / mi for the new workspace - Hal's Burgers"); + // Then it should name the new workspace, and no individual rate, because each expense on the report can land on a different one + expect(message).toBe("distance rates updated for the new workspace - Hal's Burgers"); }); - it('should describe a report that moved to another workspace', () => { - // Given an action triggered by the report moving to another workspace - const action = buildConciergeAutoSelectDistanceRateAction({ - rate: 67, - currency: 'USD', - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - policyName: "Hal's Burgers", - changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.REPORT_MOVED, - }); + it('should not escape a workspace name that contains markup', () => { + // Given a workspace name that looks like markup + const action = buildConciergeAutoSelectDistanceRateAction({policyName: 'Ops'}); // When building the message const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); - // Then it should say the workspace of the report changed - expect(message).toBe("rate updated to $0.67 / mi for the new report’s workspace - Hal's Burgers"); + // Then the name should be interpolated as-is, because this helper returns plain text + expect(message).toBe('distance rates updated for the new workspace - Ops'); }); - it('should fall back to the text of the action when the rate details are missing', () => { - // Given an action without the rate details + it('should fall back to the text of the action when the workspace name is missing', () => { + // Given an action without a workspace name const backendText = 'rate updated by the backend'; - const action = buildConciergeAutoSelectDistanceRateAction({policyName: "Hal's Burgers"}, backendText); + const action = buildConciergeAutoSelectDistanceRateAction({}, backendText); // When building the message const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); @@ -1646,13 +1634,7 @@ describe('ReportActionsUtils', () => { it('should be used for the message fragments of the action', () => { // Given a CONCIERGE_AUTO_SELECT_DISTANCE_RATE action - const action = buildConciergeAutoSelectDistanceRateAction({ - rate: 67, - currency: 'USD', - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - policyName: "Hal's Burgers", - changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.WORKSPACE_CHANGED, - }); + const action = buildConciergeAutoSelectDistanceRateAction({policyName: "Hal's Burgers"}); // When getting the message fragments of the action const fragments = ReportActionsUtils.getReportActionMessageFragments(translateLocal, action); @@ -1664,13 +1646,7 @@ describe('ReportActionsUtils', () => { it('should be visible in the report', () => { // Given a CONCIERGE_AUTO_SELECT_DISTANCE_RATE action - const action = buildConciergeAutoSelectDistanceRateAction({ - rate: 67, - currency: 'USD', - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, - policyName: "Hal's Burgers", - changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.WORKSPACE_CHANGED, - }); + const action = buildConciergeAutoSelectDistanceRateAction({policyName: "Hal's Burgers"}); // Then the action should not be filtered out as an unsupported action type expect(ReportActionsUtils.shouldReportActionBeVisible(action, action.reportActionID, true)).toBe(true); diff --git a/tests/unit/ReportAlternateTextUtilsTest.ts b/tests/unit/ReportAlternateTextUtilsTest.ts index 96d4f5f02556..103f55ce3b8e 100644 --- a/tests/unit/ReportAlternateTextUtilsTest.ts +++ b/tests/unit/ReportAlternateTextUtilsTest.ts @@ -1284,11 +1284,7 @@ describe('ReportAlternateTextUtils', () => { actionName: CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE, message: [{type: 'COMMENT', text: 'rate updated by the backend'}], originalMessage: { - rate: 67, - currency: 'USD', - unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, policyName: "Hal's Burgers", - changeType: CONST.REPORT.CONCIERGE_AUTO_SELECT_DISTANCE_RATE_CHANGE_TYPE.WORKSPACE_CHANGED, }, }; await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { @@ -1312,7 +1308,7 @@ describe('ReportAlternateTextUtils', () => { }); // Then it should be built from the translation rather than the text the backend provided - expect(lastMessage).toBe("rate updated to $0.67 / mi for the new workspace - Hal's Burgers"); + expect(lastMessage).toBe("distance rates updated for the new workspace - Hal's Burgers"); }); it('ADD_AGENT_RULE action', async () => { const report: Report = createRandomReport(0, undefined); From 7730fef7b1a8a49278082c4f87f8b403085fcfdb Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Mon, 14 Sep 2026 08:46:30 +0000 Subject: [PATCH 13/20] Leave getForDistanceRequest and DISTANCE_MERCHANT untouched The backend sends the MODIFIEDEXPENSE action, so the existing message generation handles it. Scope this PR to displaying CONCIERGE_AUTO_SELECT_DISTANCE_RATE in the report chat. Co-authored-by: dmkt9 --- src/CONST/index.ts | 1 + src/libs/ModifiedExpenseMessage.ts | 35 +++------- tests/unit/ModifiedExpenseMessageTest.ts | 87 ------------------------ 3 files changed, 9 insertions(+), 114 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e38f1f4f2729..79990f224e80 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -5489,6 +5489,7 @@ const CONST = { OTHER_INVISIBLE_CHARACTERS: /[\u3164\u115f\u1160\uffa0\u2800]/g, SHORT_MENTION_HTML: /(.*?)<\/mention-short>/g, REPORT_ID_FROM_PATH: /(? value.trim()); - const newValues = newMerchant.split(CONST.DISTANCE_MERCHANT_SEPARATOR).map((value) => value.trim()); - - // Both merchants have to be " @ " for either half to be comparable. This is a structural check rather than a format one on purpose: matching the rendered - // merchant against a regex also asserts the number formatting, which is localized, so it rejected valid merchants in every locale that writes decimals with a comma. - if (oldValues.length === 2 && newValues.length === 2 && oldValues.every(Boolean) && newValues.every(Boolean)) { - const [oldDistance, oldUnit] = splitDistanceAndUnit(oldValues.at(0) ?? ''); - const [newDistance, newUnit] = splitDistanceAndUnit(newValues.at(0) ?? ''); - const oldRate = oldValues.at(1); - const newRate = newValues.at(1); - - // A changed unit means the journey was re-expressed, not re-measured, so the distance itself is unchanged even though the number is. That is what happens when an - // expense lands on a workspace whose rates are in the other unit, and it used to be reported as a distance change. - const didDistanceChange = oldUnit === newUnit && oldDistance !== newDistance; + if (CONST.REGEX.DISTANCE_MERCHANT.test(newMerchant) && CONST.REGEX.DISTANCE_MERCHANT.test(oldMerchant)) { + const oldValues = oldMerchant.split('@'); + const oldDistance = oldValues.at(0)?.trim() ?? ''; + const oldRate = oldValues.at(1)?.trim() ?? ''; + const newValues = newMerchant.split('@'); + const newDistance = newValues.at(0)?.trim() ?? ''; + const newRate = newValues.at(1)?.trim() ?? ''; - if (!didDistanceChange && oldRate !== newRate) { + if (oldDistance === newDistance && oldRate !== newRate) { changedField = 'rate'; } } else { diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index 2821c9b10d68..5de3d626f9a2 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -957,93 +957,6 @@ describe('ModifiedExpenseMessage', () => { }); }); - describe('when the distance rate is changed and the new rate is in another unit', () => { - const reportAction = { - ...createRandomReportAction(1), - actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, - originalMessage: { - oldMerchant: '10.00 mi @ $0.50 / mi', - merchant: '16.09 km @ $0.67 / km', - oldAmount: 500, - amount: 1078, - oldCurrency: CONST.CURRENCY.USD, - currency: CONST.CURRENCY.USD, - }, - }; - - it('then the message still says the rate is changed, because the journey was re-expressed rather than re-measured', () => { - const expectedResult = `changed the rate to ${reportAction.originalMessage.merchant} (previously ${reportAction.originalMessage.oldMerchant}), which updated the amount to $10.78 (previously $5.00)`; - const result = getForReportAction({ - convertToDisplayString, - translate: translateLocal, - reportAction, - policy: undefined, - policyTags: undefined, - currentUserAccountID: CURRENT_USER_ACCOUNT_ID, - currentUserLogin: CURRENT_USER_LOGIN, - }); - expect(result).toEqual(expectedResult); - }); - }); - - describe('when the distance rate is changed and the rate is formatted with a decimal comma', () => { - const reportAction = { - ...createRandomReportAction(1), - actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, - originalMessage: { - oldMerchant: '56.36 mi @ $0,70 / mi', - merchant: '56.36 mi @ $0,99 / mi', - oldAmount: 3945, - amount: 5580, - oldCurrency: CONST.CURRENCY.USD, - currency: CONST.CURRENCY.USD, - }, - }; - - it('then the message says the rate is changed, because the comparison does not assume a decimal point', () => { - const expectedResult = `changed the rate to ${reportAction.originalMessage.merchant} (previously ${reportAction.originalMessage.oldMerchant}), which updated the amount to $55.80 (previously $39.45)`; - const result = getForReportAction({ - convertToDisplayString, - translate: translateLocal, - reportAction, - policy: undefined, - policyTags: undefined, - currentUserAccountID: CURRENT_USER_ACCOUNT_ID, - currentUserLogin: CURRENT_USER_LOGIN, - }); - expect(result).toEqual(expectedResult); - }); - }); - - describe('when the old merchant is not a distance merchant', () => { - const reportAction = { - ...createRandomReportAction(1), - actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, - originalMessage: { - oldMerchant: 'Pending...', - merchant: '56.36 mi @ $0.99 / mi', - oldAmount: 3945, - amount: 5580, - oldCurrency: CONST.CURRENCY.USD, - currency: CONST.CURRENCY.USD, - }, - }; - - it('then the message falls back to describing a distance change, because neither half is comparable', () => { - const expectedResult = `changed the distance to ${reportAction.originalMessage.merchant} (previously ${reportAction.originalMessage.oldMerchant}), which updated the amount to $55.80 (previously $39.45)`; - const result = getForReportAction({ - convertToDisplayString, - translate: translateLocal, - reportAction, - policy: undefined, - policyTags: undefined, - currentUserAccountID: CURRENT_USER_ACCOUNT_ID, - currentUserLogin: CURRENT_USER_LOGIN, - }); - expect(result).toEqual(expectedResult); - }); - }); - describe('when moving an expense', () => { it('returns the movedFromOrToReportMessage message when provided', () => { const reportAction = { From 05013415eb5d5f3c631334a3f9d9627ff2650458 Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Mon, 14 Sep 2026 09:36:12 +0000 Subject: [PATCH 14/20] Apply the Polyglot Parrot translations for conciergeAutoSelectedDistanceRates Co-authored-by: dmkt9 --- src/languages/de.ts | 2 +- src/languages/el.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/zh-hans.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index dff2940a9a83..aa94c4962b50 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1862,7 +1862,7 @@ const translations: TranslationDeepObject = { prompt: 'Aktivieren Sie Tags im Workspace, um die Ausgabendetails zu bearbeiten oder den Tag aus dieser Ausgabe zu löschen.', confirmText: 'Tag löschen', }, - conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `Entfernungssätze für den neuen Workspace „${policyName}“ aktualisiert`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `Kilometersätze für den neuen Arbeitsbereich aktualisiert – ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/el.ts b/src/languages/el.ts index 27c982afe769..37c364287467 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -1912,7 +1912,7 @@ const translations: TranslationDeepObject = { whatIsHoldExplainDM: 'Η αναμονή είναι σαν να πατάτε «παύση» σε μία δαπάνη μέχρι να είστε έτοιμοι να τη στείλετε.', holdIsLeftBehindDM: 'Οι δεσμευμένες δαπάνες δεν θα αποσταλούν μέχρι να καταργήσετε τη δέσμευση.', unholdWhenReadyDM: 'Αποδεσμεύστε τις δαπάνες όταν είστε έτοιμοι να τις στείλετε.', - conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `οι συντελεστές απόστασης ενημερώθηκαν για τον νέο χώρο εργασίας - ${policyName}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `οι τιμές αποζημίωσης χιλιομέτρων ενημερώθηκαν για το νέο χώρο εργασίας - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 62c290083bb6..1000437ea206 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1853,7 +1853,7 @@ const translations: TranslationDeepObject = { prompt: 'Habilita las etiquetas en el espacio de trabajo para editar los detalles del gasto o eliminar la etiqueta de este gasto.', confirmText: 'Eliminar etiqueta', }, - conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `se actualizaron las tasas de distancia para el nuevo espacio de trabajo - ${policyName}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `se han actualizado las tarifas de kilometraje para el nuevo espacio de trabajo: ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index a9bdfd369e04..6cbc4b2e0d2c 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1868,7 +1868,7 @@ const translations: TranslationDeepObject = { prompt: 'Active les tags dans l’espace de travail pour modifier les détails de la dépense ou supprimer le tag de cette dépense.', confirmText: 'Supprimer le tag', }, - conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `taux kilométriques mis à jour pour le nouvel espace de travail - ${policyName}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `les taux kilométriques ont été mis à jour pour le nouvel espace de travail - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/it.ts b/src/languages/it.ts index fefab3f0880b..1252c3ec2be3 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1858,7 +1858,7 @@ const translations: TranslationDeepObject = { prompt: 'Abilita le etichette nello spazio di lavoro per modificare i dettagli della spesa o eliminare l’etichetta da questa spesa.', confirmText: 'Elimina tag', }, - conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `tariffe di distanza aggiornate per il nuovo workspace - ${policyName}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `tariffe chilometriche aggiornate per il nuovo spazio di lavoro - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 8404d57b0147..ab6bd2df19c5 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1840,7 +1840,7 @@ const translations: TranslationDeepObject = { prompt: 'ワークスペースでタグを有効にすると、この経費の詳細を編集したり、この経費からタグを削除したりできます。', confirmText: 'タグを削除', }, - conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `新しいワークスペース「${policyName}」の距離レートが更新されました`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `新しいワークスペース「${policyName}」の距離単価を更新しました`, }, transactionMerge: { listPage: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 2e5cab65b7ca..a5519685a108 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1854,7 +1854,7 @@ const translations: TranslationDeepObject = { prompt: 'Schakel tags in op de werkruimte om de onkostendetails te bewerken of de tag uit deze onkosten te verwijderen.', confirmText: 'Label verwijderen', }, - conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `afstandstarieven bijgewerkt voor de nieuwe workspace - ${policyName}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `kilometervergoedingen bijgewerkt voor de nieuwe workspace - ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 63d265cff6cf..138f32da2b9f 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1889,7 +1889,7 @@ const translations: TranslationDeepObject = { prompt: 'Włącz tagi w przestrzeni roboczej, aby edytować szczegóły wydatku lub usunąć ten tag z tego wydatku.', confirmText: 'Usuń znacznik', }, - conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `stawki za odległość zaktualizowane dla nowego workspace’u – ${policyName}`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `stawki za przejechany dystans zaktualizowane dla nowej przestrzeni roboczej – ${policyName}`, }, transactionMerge: { listPage: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index b84c53b90bcd..ff88e2ad9722 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1782,7 +1782,7 @@ const translations: TranslationDeepObject = { deleteConfirmationSomePendingBYOC: '您确定要删除这些报销吗?其中一些处于待处理状态,如果入账后,我们可能会再次导入。', categoryDisabledAlert: {title: '类别已禁用', prompt: '在工作区中启用类别,以编辑报销详情或从此报销中删除该类别。', confirmText: '删除类别'}, tagDisabledAlert: {title: '标签已停用', prompt: '请在工作区中启用标签,以便编辑该报销的详细信息或从此报销中删除该标签。', confirmText: '删除标签'}, - conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `新工作区 ${policyName} 的距离费率已更新`, + conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `已为新工作区更新里程报销标准 - ${policyName}`, }, transactionMerge: { listPage: { From fd6040482150a0fd1146e99a03f5c25a56da8ceb Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Mon, 14 Sep 2026 10:34:05 +0000 Subject: [PATCH 15/20] Fix the typecheck and ESLint failures after merging main - getLastMessageTextForReport gained a required rules param on main, so the CONCIERGE_AUTO_SELECT_DISTANCE_RATE test has to pass it like its siblings. - Narrow the action with isActionOfType instead of asserting it, which keeps no-unsafe-type-assertion within the seatbelt count for this file. Co-authored-by: dmkt9 --- src/libs/ReportActionsUtils.ts | 2 +- tests/unit/ReportAlternateTextUtilsTest.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 4b2a8a05ccef..0edf257f0bb2 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -3440,7 +3440,7 @@ function getWorkspaceCustomUnitRateUpdatedMessage(translate: LocalizedTranslate, * Builds the Concierge system message explaining that the distance rates of a report's expenses were re-selected automatically. */ function getConciergeAutoSelectDistanceRateMessage(translate: LocalizedTranslate, action: ReportAction): string { - const {policyName} = getOriginalMessage(action as ReportAction) ?? {}; + const policyName = isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE) ? getOriginalMessage(action)?.policyName : undefined; if (!policyName) { return getReportActionText(action); diff --git a/tests/unit/ReportAlternateTextUtilsTest.ts b/tests/unit/ReportAlternateTextUtilsTest.ts index aea726724e4a..ca0df31c88e1 100644 --- a/tests/unit/ReportAlternateTextUtilsTest.ts +++ b/tests/unit/ReportAlternateTextUtilsTest.ts @@ -1311,6 +1311,7 @@ describe('ReportAlternateTextUtils', () => { // When getting the last message text of the report const lastMessage = getLastMessageTextForReport({ + rules: undefined, dateFnsLocale: undefined, convertToDisplayString, conciergeReportID: undefined, From 9dea87ede70fafc7c4b04e302577aaf21c9b496b Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Mon, 14 Sep 2026 10:51:15 +0000 Subject: [PATCH 16/20] Drop the @context annotation for conciergeAutoSelectedDistanceRates Co-authored-by: dmkt9 --- src/languages/en.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 94c99d4a4a46..b4271d4e2f8f 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1919,7 +1919,6 @@ const translations = { correctRateError: 'Fix the rate error and try again.', AskToExplain: `. Explain`, conciergeAutoMatchedVendor: ({vendorName}: {vendorName: string}) => `Concierge matched this expense to ${vendorName}`, - // @context "distance rates" are the per-mile or per-kilometer amounts a workspace reimburses for mileage, not prices, fees or ratings. Lowercase because the sentence continues a system message. conciergeAutoSelectedDistanceRates: ({policyName}: {policyName: string}) => `distance rates updated for the new workspace - ${policyName}`, rulesModifiedFields: { reimbursable: (value: boolean) => (value ? 'marked the expense as "reimbursable"' : 'marked the expense as "non-reimbursable"'), From 02030a7446443c928aae67318572a06aabdb3f22 Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Mon, 14 Sep 2026 13:18:15 +0000 Subject: [PATCH 17/20] Encode the Concierge distance rate message at its HTML sinks Co-authored-by: dmkt9 --- src/libs/ReportActionsUtils.ts | 3 ++- .../report/ContextMenu/ContextMenuActions.tsx | 3 ++- .../actionContents/ActionContentRouter.tsx | 4 +++- tests/unit/ReportActionsUtilsTest.ts | 22 +++++++++++++++++-- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 0edf257f0bb2..bd42588a12de 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -2527,7 +2527,8 @@ function getReportActionMessageFragments(translate: LocalizedTranslate, action: if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE)) { const message = getConciergeAutoSelectDistanceRateMessage(translate, action); - return [{text: message, html: `${message}`, type: 'COMMENT'}]; + // The helper returns plain text, so only the html fragment is encoded — a workspace name containing an entity like `©` would otherwise be parsed as markup. + return [{text: message, html: `${Str.htmlEncode(message)}`, type: 'COMMENT'}]; } if (isDynamicExternalWorkflowSubmitFailedAction(action)) { diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx index 4c9635941e2f..1b3cfcf86149 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx @@ -1381,7 +1381,8 @@ const ContextMenuActions: ContextMenuAction[] = [ } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.TRAVEL_UPDATE)) { setClipboardMessage(getTravelUpdateMessage(translate, reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE)) { - setClipboardMessage(getConciergeAutoSelectDistanceRateMessage(translate, reportAction)); + // setClipboardMessage treats its argument as HTML, and the helper returns plain text, so encode it to copy a workspace name containing an entity literally. + setClipboardMessage(Str.htmlEncode(getConciergeAutoSelectDistanceRateMessage(translate, reportAction))); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUDIT_RATE)) { setClipboardMessage(getUpdatedAuditRateMessage(translate, reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_APPROVER_RULE)) { diff --git a/src/pages/inbox/report/actionContents/ActionContentRouter.tsx b/src/pages/inbox/report/actionContents/ActionContentRouter.tsx index 67521642444e..8d6a20525ced 100644 --- a/src/pages/inbox/report/actionContents/ActionContentRouter.tsx +++ b/src/pages/inbox/report/actionContents/ActionContentRouter.tsx @@ -62,6 +62,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {OnyxEntry} from 'react-native-onyx'; +import {Str} from 'expensify-common'; import React from 'react'; import ApprovalFlowContent, {isApprovalFlowAction} from './ApprovalFlowContent'; @@ -364,7 +365,8 @@ function ActionContentRouter({ if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.CONCIERGE_AUTO_SELECT_DISTANCE_RATE)) { return ( - ${getConciergeAutoSelectDistanceRateMessage(translate, action)}`} /> + {/* The helper returns plain text, so encode it before it becomes HTML or a workspace name containing an entity like `©` would be parsed as markup. */} + ${Str.htmlEncode(getConciergeAutoSelectDistanceRateMessage(translate, action))}`} /> ); } diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 3fa589e8e494..cb1f8268c631 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -4,6 +4,7 @@ import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import getReportURLForCurrentContext from '@libs/Navigation/helpers/getReportURLForCurrentContext'; import {setHasRadio} from '@libs/NetworkState'; +import Parser from '@libs/Parser'; import {isExpenseReport} from '@libs/ReportUtils'; import IntlStore from '@src/languages/IntlStore'; @@ -13,6 +14,8 @@ import type {ValueOf} from 'type-fest'; import Onyx from 'react-native-onyx'; +import {Str} from 'expensify-common'; + import type {CompanyAddressOriginalMessage, UpdateACHAccountOriginalMessage} from '../../src/libs/ReportActionsUtils'; import type {Card, DecisionName, PersonalDetails, PersonalDetailsList, Report, ReportAction, ReportActions} from '../../src/types/onyx'; import type {OriginalMessageExportIntegration} from '../../src/types/onyx/OriginalMessage'; @@ -1639,9 +1642,24 @@ describe('ReportActionsUtils', () => { // When getting the message fragments of the action const fragments = ReportActionsUtils.getReportActionMessageFragments(translateLocal, action); - // Then they should be built from the same message + // Then the text fragment should be the message as-is, and the html fragment should carry its encoded form const message = ReportActionsUtils.getConciergeAutoSelectDistanceRateMessage(translateLocal, action); - expect(fragments).toEqual([{text: message, html: `${message}`, type: 'COMMENT'}]); + expect(fragments).toEqual([{text: message, html: `${Str.htmlEncode(message)}`, type: 'COMMENT'}]); + }); + + it('should keep a workspace name containing an HTML entity literal on the html fragment', () => { + // Given a workspace name that contains an entity-shaped substring, which workspace name validation allows because it only rejects angle-bracket tags + const action = buildConciergeAutoSelectDistanceRateAction({policyName: 'R&D ©'}); + + // When getting the message fragments of the action + const fragments = ReportActionsUtils.getReportActionMessageFragments(translateLocal, action); + + // Then the text fragment should stay plain + const message = 'distance rates updated for the new workspace - R&D ©'; + expect(fragments.at(0)?.text).toBe(message); + + // And decoding the html fragment should give that same string back, rather than parsing the entity into a copyright sign + expect(Parser.htmlToText(fragments.at(0)?.html ?? '')).toBe(message); }); it('should be visible in the report', () => { From 6286dc6a9309466ca9afed89c01d4d35c71ed77c Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Mon, 14 Sep 2026 13:29:41 +0000 Subject: [PATCH 18/20] Sort the Str import to satisfy oxfmt Co-authored-by: dmkt9 --- tests/unit/ReportActionsUtilsTest.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index cb1f8268c631..2cd9953b9e59 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -12,9 +12,8 @@ import ROUTES from '@src/ROUTES'; import type {ValueOf} from 'type-fest'; -import Onyx from 'react-native-onyx'; - import {Str} from 'expensify-common'; +import Onyx from 'react-native-onyx'; import type {CompanyAddressOriginalMessage, UpdateACHAccountOriginalMessage} from '../../src/libs/ReportActionsUtils'; import type {Card, DecisionName, PersonalDetails, PersonalDetailsList, Report, ReportAction, ReportActions} from '../../src/types/onyx'; From 902f31bb553876f0177342353d6ccd4da307207e Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Tue, 15 Sep 2026 02:09:46 +0000 Subject: [PATCH 19/20] Let the distance merchant regex accept grouped and locale-formatted numbers A modified expense whose merchant carries a thousands separator (1,234.56 mi) failed CONST.REGEX.DISTANCE_MERCHANT, so getForDistanceRequest fell through to its else branch and described a rate-only change as a distance change. Co-authored-by: dmkt9 --- src/CONST/index.ts | 4 +- tests/unit/ModifiedExpenseMessageTest.ts | 66 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index f2d1ef22aaf7..4aa61d502442 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -5654,7 +5654,9 @@ const CONST = { OTHER_INVISIBLE_CHARACTERS: /[\u3164\u115f\u1160\uffa0\u2800]/g, SHORT_MENTION_HTML: /(.*?)<\/mention-short>/g, REPORT_ID_FROM_PATH: /(? { }); }); + describe('when only the rate of a distance expense changes', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + merchant: '1,234.56 mi @ $0.70 / mi', + oldMerchant: '1,234.56 mi @ $0.67 / mi', + amount: 86419, + currency: CONST.CURRENCY.USD, + oldAmount: 82716, + oldCurrency: CONST.CURRENCY.USD, + }, + }; + + // The backend groups the distance with a thousands separator, so the merchant only matches + // DISTANCE_MERCHANT once that separator is allowed. Without it the message wrongly says "distance". + it('says the rate changed even when the distance carries a thousands separator', () => { + const expectedResult = + 'changed the rate to 1,234.56 mi @ $0.70 / mi (previously 1,234.56 mi @ $0.67 / mi), which updated the amount to $864.19 (previously $827.16)'; + + const result = getForReportAction({ + convertToDisplayString, + translate: translateLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + currentUserLogin: CURRENT_USER_LOGIN, + }); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when only the distance of a distance expense changes', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + merchant: '2,345.67 mi @ $0.67 / mi', + oldMerchant: '1,234.56 mi @ $0.67 / mi', + amount: 157159, + currency: CONST.CURRENCY.USD, + oldAmount: 82716, + oldCurrency: CONST.CURRENCY.USD, + }, + }; + + it('says the distance changed', () => { + const expectedResult = + 'changed the distance to 2,345.67 mi @ $0.67 / mi (previously 1,234.56 mi @ $0.67 / mi), which updated the amount to $1,571.59 (previously $827.16)'; + + const result = getForReportAction({ + convertToDisplayString, + translate: translateLocal, + reportAction, + policy: undefined, + policyTags: undefined, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + currentUserLogin: CURRENT_USER_LOGIN, + }); + + expect(result).toEqual(expectedResult); + }); + }); + describe('when the amount and merchant are changed', () => { const reportAction = { ...createRandomReportAction(1), From 344a7960ff25e1aef3acb6ae54e21a8bf4922904 Mon Sep 17 00:00:00 2001 From: "dmkt9 (via MelvinBot)" Date: Tue, 15 Sep 2026 02:25:51 +0000 Subject: [PATCH 20/20] Collapse the two expectedResult assignments onto one line for oxfmt Co-authored-by: dmkt9 --- tests/unit/ModifiedExpenseMessageTest.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index fd469a069d52..13aceb1d4b61 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -449,8 +449,7 @@ describe('ModifiedExpenseMessage', () => { // The backend groups the distance with a thousands separator, so the merchant only matches // DISTANCE_MERCHANT once that separator is allowed. Without it the message wrongly says "distance". it('says the rate changed even when the distance carries a thousands separator', () => { - const expectedResult = - 'changed the rate to 1,234.56 mi @ $0.70 / mi (previously 1,234.56 mi @ $0.67 / mi), which updated the amount to $864.19 (previously $827.16)'; + const expectedResult = 'changed the rate to 1,234.56 mi @ $0.70 / mi (previously 1,234.56 mi @ $0.67 / mi), which updated the amount to $864.19 (previously $827.16)'; const result = getForReportAction({ convertToDisplayString, @@ -481,8 +480,7 @@ describe('ModifiedExpenseMessage', () => { }; it('says the distance changed', () => { - const expectedResult = - 'changed the distance to 2,345.67 mi @ $0.67 / mi (previously 1,234.56 mi @ $0.67 / mi), which updated the amount to $1,571.59 (previously $827.16)'; + const expectedResult = 'changed the distance to 2,345.67 mi @ $0.67 / mi (previously 1,234.56 mi @ $0.67 / mi), which updated the amount to $1,571.59 (previously $827.16)'; const result = getForReportAction({ convertToDisplayString,