From aa1240cb1bbd3aa1fcb2b3c64d0ea70832433a95 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Thu, 26 Feb 2026 17:56:10 +0000 Subject: [PATCH 1/2] Strip accounting-style parentheses from expense report names in LHN The backend formats negative expense amounts with accounting-style parentheses (e.g., "($25.00)") in report.reportName. This caused the LHN preview to display amounts in brackets. Add stripAccountingBrackets helper to normalize these in getMoneyRequestReportName and computeReportName. Also fix buildOptimisticExpenseReport to use Math.abs for the display amount. Co-authored-by: Aimane Chnaif --- src/libs/ReportNameUtils.ts | 13 +++++++++++-- src/libs/ReportUtils.ts | 2 +- tests/unit/ReportNameUtilsTest.ts | 19 +++++++++++++++++++ tests/unit/ReportUtilsTest.ts | 2 +- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index 4d318f77c5415..2e4058584762f 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -149,6 +149,15 @@ Onyx.connect({ }, }); +/** + * Strips accounting-style parentheses from currency amounts in report names. + * The backend formats negative amounts with parentheses (e.g., "($25.00)") instead of + * a minus sign. This function converts them to the standard format (e.g., "$25.00"). + */ +function stripAccountingBrackets(reportName: string): string { + return reportName.replace(/\(([^)]*\d[^)]*)\)/g, '$1'); +} + function generateArchivedReportName(reportName: string): string { // eslint-disable-next-line @typescript-eslint/no-deprecated return `${reportName} (${translateLocal('common.archived')}) `; @@ -335,7 +344,7 @@ function getMoneyRequestReportName({report, policy, invoiceReceiverPolicy}: {rep } if (report?.reportName && isExpenseReport(report)) { - return report.reportName; + return stripAccountingBrackets(report.reportName); } const moneyRequestTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend; @@ -780,7 +789,7 @@ function computeReportName( } if (report?.reportName && report.type === CONST.REPORT.TYPE.EXPENSE) { - return report?.reportName; + return stripAccountingBrackets(report.reportName); } if (isTaskReport(report)) { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 14666a3201f96..bb0e4a6d4b96d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6727,7 +6727,7 @@ function buildOptimisticExpenseReport({ const storedNonReimbursableTotal = nonReimbursableTotal * -1; const report = chatReportID ? getReportOrDraftReport(chatReportID) : undefined; const policyName = getPolicyName({report}); - const formattedTotal = convertToDisplayString(storedTotal, currency); + const formattedTotal = convertToDisplayString(Math.abs(storedTotal), currency); // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line @typescript-eslint/no-deprecated const policyReal = getPolicy(policyID); diff --git a/tests/unit/ReportNameUtilsTest.ts b/tests/unit/ReportNameUtilsTest.ts index c99d6004434f3..1a2c137d6ef3c 100644 --- a/tests/unit/ReportNameUtilsTest.ts +++ b/tests/unit/ReportNameUtilsTest.ts @@ -744,6 +744,25 @@ describe('ReportNameUtils', () => { expect(reportName).toBe(CONST.REPORT.DEFAULT_EXPENSE_REPORT_NAME); }); + it('should strip accounting-style parentheses from expense report reportName', () => { + // Given an expense report with a reportName containing accounting-style parenthesized amount + const expenseReport: Report = { + ...createExpenseReport(202), + reportID: '202', + reportName: 'Workspace owes ($25.00)', + policyID: '202', + type: CONST.REPORT.TYPE.EXPENSE, + total: -2500, + currency: 'USD', + }; + + // When we get the money request report name + const reportName = getMoneyRequestReportName({report: expenseReport}); + + // Then it should strip the accounting-style parentheses + expect(reportName).toBe('Workspace owes $25.00'); + }); + it('should not return empty string for expense report with empty reportName when policy has a normal fieldList', () => { // Given an expense report with empty reportName const expenseReport: Report = { diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 071e3c90a7c91..d8bc958c9b15f 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -10098,7 +10098,7 @@ describe('ReportUtils', () => { const total = 100; const currency = CONST.CURRENCY.USD; const expenseReport = buildOptimisticExpenseReport({chatReportID, policyID: undefined, payeeAccountID: 1, total, currency, betas: [CONST.BETAS.ALL]}); - expect(expenseReport.reportName).toBe(`${fakePolicy.name} owes ${convertToDisplayString(-total, currency)}`); + expect(expenseReport.reportName).toBe(`${fakePolicy.name} owes ${convertToDisplayString(total, currency)}`); }); it('should set reportName to "New Report" when policy field list is empty', async () => { From 0d1a6caf4fc4b128e54c2f2b826ee5e98bcec0c1 Mon Sep 17 00:00:00 2001 From: MelvinBot Date: Thu, 26 Feb 2026 18:01:10 +0000 Subject: [PATCH 2/2] Fix: Use replaceAll and fix Prettier formatting - Change String#replace() to String#replaceAll() in stripAccountingBrackets to satisfy unicorn/prefer-string-replace-all - Fix import ordering in getClipboardTextTest.ts for Prettier Co-authored-by: Aimane Chnaif --- src/libs/ReportNameUtils.ts | 2 +- tests/unit/libs/getClipboardTextTest.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index 2e4058584762f..a328ac95d1ae5 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -155,7 +155,7 @@ Onyx.connect({ * a minus sign. This function converts them to the standard format (e.g., "$25.00"). */ function stripAccountingBrackets(reportName: string): string { - return reportName.replace(/\(([^)]*\d[^)]*)\)/g, '$1'); + return reportName.replaceAll(/\(([^)]*\d[^)]*)\)/g, '$1'); } function generateArchivedReportName(reportName: string): string { diff --git a/tests/unit/libs/getClipboardTextTest.ts b/tests/unit/libs/getClipboardTextTest.ts index 042120d6357b8..15c2480f9aed1 100644 --- a/tests/unit/libs/getClipboardTextTest.ts +++ b/tests/unit/libs/getClipboardTextTest.ts @@ -1,5 +1,5 @@ -import Parser from '@libs/Parser'; import getClipboardText from '@libs/Clipboard/getClipboardText'; +import Parser from '@libs/Parser'; jest.mock('@libs/Parser', () => ({ // eslint-disable-next-line @typescript-eslint/naming-convention