From 77aa2bb68b40e621b3eef66e77113d984aaff1d4 Mon Sep 17 00:00:00 2001 From: DebuggingMax Date: Thu, 26 Feb 2026 21:00:55 +0000 Subject: [PATCH 1/3] fix: clear convertedAmount when amount or currency is manually modified When a user manually edits an expense's amount or currency, the convertedAmount field was not being cleared. This caused incorrect currency conversion to be applied during report export - the system would re-convert already manually entered amounts. For example, if a user manually entered C$62.85 (matching their credit card charge), the export would incorrectly convert this again, resulting in C$85.63. This fix clears convertedAmount when modifiedAmount or modifiedCurrency is set, ensuring the server recalculates conversion if needed rather than using stale data. Fixes #83451 --- src/libs/TransactionUtils/index.ts | 7 +++++ tests/unit/TransactionUtilsTest.ts | 42 ++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 2348aad087b73..c6d699f75b650 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -699,10 +699,17 @@ function getUpdatedTransaction({ if (Object.hasOwn(transactionChanges, 'amount') && typeof transactionChanges.amount === 'number') { updatedTransaction.modifiedAmount = isFromExpenseReport || isUnReportedExpense ? -transactionChanges.amount : transactionChanges.amount; shouldStopSmartscan = true; + // Clear convertedAmount when amount is manually modified to prevent stale conversion data + // from being used during export (the server will recalculate if needed) + updatedTransaction.convertedAmount = undefined; } if (Object.hasOwn(transactionChanges, 'currency')) { updatedTransaction.modifiedCurrency = transactionChanges.currency; shouldStopSmartscan = true; + // Clear convertedAmount when currency is manually modified to prevent incorrect + // currency conversion being applied during export (fixes issue where manually entered + // amounts in a different currency were being re-converted) + updatedTransaction.convertedAmount = undefined; } if (Object.hasOwn(transactionChanges, 'merchant')) { diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index 5a4283fd5f498..a37a7547a2edd 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -425,6 +425,48 @@ describe('TransactionUtils', () => { merchant: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }); }); + + it('should clear convertedAmount when currency is changed to prevent incorrect export conversion', () => { + // Given: a transaction with a converted amount (simulating a foreign currency transaction) + const transaction = generateTransaction({ + amount: -4500, // $45.00 USD + currency: CONST.CURRENCY.USD, + convertedAmount: -6285, // C$62.85 CAD (server-calculated conversion) + }); + + // When: user manually changes the currency to CAD + const updatedTransaction = TransactionUtils.getUpdatedTransaction({ + transaction, + isFromExpenseReport: true, + transactionChanges: {currency: 'CAD'}, + }); + + // Then: convertedAmount should be cleared to prevent stale conversion data + // being incorrectly applied during export + expect(updatedTransaction.modifiedCurrency).toBe('CAD'); + expect(updatedTransaction.convertedAmount).toBeUndefined(); + }); + + it('should clear convertedAmount when amount is manually changed to prevent incorrect export conversion', () => { + // Given: a transaction with a converted amount + const transaction = generateTransaction({ + amount: -4500, // $45.00 USD + currency: CONST.CURRENCY.USD, + convertedAmount: -6285, // C$62.85 CAD + }); + + // When: user manually changes the amount + const updatedTransaction = TransactionUtils.getUpdatedTransaction({ + transaction, + isFromExpenseReport: true, + transactionChanges: {amount: 6285}, // C$62.85 + }); + + // Then: convertedAmount should be cleared since the manual amount + // is the final value and shouldn't be re-converted during export + expect(updatedTransaction.modifiedAmount).toBe(-6285); + expect(updatedTransaction.convertedAmount).toBeUndefined(); + }); }); describe('getTransactionType', () => { From e3ff6037e481526215ccff0759cf81aadc469783 Mon Sep 17 00:00:00 2001 From: DebuggingMax Date: Fri, 27 Feb 2026 05:50:56 +0000 Subject: [PATCH 2/3] chore: trigger CI re-run From ccc01f43fef26297bfe07d9df227c2b52da51d21 Mon Sep 17 00:00:00 2001 From: DebuggingMax Date: Fri, 27 Feb 2026 05:52:49 +0000 Subject: [PATCH 3/3] fix: use null instead of undefined for clearing convertedAmount Addresses review feedback: Onyx.merge treats undefined as no-op, so setting convertedAmount to undefined doesn't actually remove the value. Using null ensures the stale conversion data is properly cleared. Updated type definition to allow null, and updated tests accordingly. --- src/libs/TransactionUtils/index.ts | 6 ++++-- src/types/onyx/Transaction.ts | 2 +- tests/unit/TransactionUtilsTest.ts | 10 ++++++---- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index c6d699f75b650..c8c770077898e 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -701,7 +701,8 @@ function getUpdatedTransaction({ shouldStopSmartscan = true; // Clear convertedAmount when amount is manually modified to prevent stale conversion data // from being used during export (the server will recalculate if needed) - updatedTransaction.convertedAmount = undefined; + // Use null instead of undefined because Onyx.merge treats undefined as no-op + updatedTransaction.convertedAmount = null; } if (Object.hasOwn(transactionChanges, 'currency')) { updatedTransaction.modifiedCurrency = transactionChanges.currency; @@ -709,7 +710,8 @@ function getUpdatedTransaction({ // Clear convertedAmount when currency is manually modified to prevent incorrect // currency conversion being applied during export (fixes issue where manually entered // amounts in a different currency were being re-converted) - updatedTransaction.convertedAmount = undefined; + // Use null instead of undefined because Onyx.merge treats undefined as no-op + updatedTransaction.convertedAmount = null; } if (Object.hasOwn(transactionChanges, 'merchant')) { diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 58fcb8c57f9f9..694e1b873711b 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -441,7 +441,7 @@ type Transaction = OnyxCommon.OnyxValueWithOfflineFeedback< accountant?: Accountant; /** The transaction converted amount in report's currency */ - convertedAmount?: number; + convertedAmount?: number | null; /** The transaction tax amount */ taxAmount?: number; diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index a37a7547a2edd..e42b9d3c0c4ce 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -441,10 +441,11 @@ describe('TransactionUtils', () => { transactionChanges: {currency: 'CAD'}, }); - // Then: convertedAmount should be cleared to prevent stale conversion data + // Then: convertedAmount should be cleared (set to null, not undefined, because + // Onyx.merge treats undefined as no-op) to prevent stale conversion data // being incorrectly applied during export expect(updatedTransaction.modifiedCurrency).toBe('CAD'); - expect(updatedTransaction.convertedAmount).toBeUndefined(); + expect(updatedTransaction.convertedAmount).toBeNull(); }); it('should clear convertedAmount when amount is manually changed to prevent incorrect export conversion', () => { @@ -462,10 +463,11 @@ describe('TransactionUtils', () => { transactionChanges: {amount: 6285}, // C$62.85 }); - // Then: convertedAmount should be cleared since the manual amount + // Then: convertedAmount should be cleared (set to null, not undefined, because + // Onyx.merge treats undefined as no-op) since the manual amount // is the final value and shouldn't be re-converted during export expect(updatedTransaction.modifiedAmount).toBe(-6285); - expect(updatedTransaction.convertedAmount).toBeUndefined(); + expect(updatedTransaction.convertedAmount).toBeNull(); }); });