From e24b2d1dc39ac4004b38fdc48957c2668283dc58 Mon Sep 17 00:00:00 2001 From: "Fitsum Abebe (via MelvinBot)" Date: Tue, 21 Apr 2026 20:33:18 +0000 Subject: [PATCH 1/5] Fix: sync taxValue when category with default tax rate is selected When selecting a category with a default tax rate, getCategoryTaxCodeAndAmount updates taxCode and taxAmount but not taxValue. This mismatch causes the tax field to display "0%" and triggers "Tax no longer valid" on submit. Return categoryTaxValue from getCategoryTaxCodeAndAmount and use setMoneyRequestTaxRateValues to set all three fields atomically. Co-authored-by: Fitsum Abebe --- src/libs/TransactionUtils/index.ts | 9 ++++++--- src/libs/actions/IOU/index.ts | 8 +++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 575d7db9cbee..4735d6bd1ddf 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -840,10 +840,13 @@ function getUpdatedTransaction({ if (Object.hasOwn(transactionChanges, 'category') && typeof transactionChanges.category === 'string') { updatedTransaction.category = transactionChanges.category; - const {categoryTaxCode, categoryTaxAmount} = getCategoryTaxCodeAndAmount(transactionChanges.category, transaction, policy); + const {categoryTaxCode, categoryTaxAmount, categoryTaxValue} = getCategoryTaxCodeAndAmount(transactionChanges.category, transaction, policy); if (categoryTaxCode && categoryTaxAmount !== undefined) { updatedTransaction.taxCode = categoryTaxCode; updatedTransaction.taxAmount = categoryTaxAmount; + if (categoryTaxValue) { + updatedTransaction.taxValue = categoryTaxValue; + } } } @@ -2596,7 +2599,7 @@ function buildMergeDuplicatesParams( function getCategoryTaxCodeAndAmount(category: string, transaction: OnyxEntry, policy: OnyxEntry) { const taxRules = policy?.rules?.expenseRules?.filter((rule) => rule.tax); if (!taxRules || taxRules?.length === 0 || isDistanceRequest(transaction)) { - return {categoryTaxCode: undefined, categoryTaxAmount: undefined}; + return {categoryTaxCode: undefined, categoryTaxAmount: undefined, categoryTaxValue: undefined}; } const defaultTaxCode = getDefaultTaxCode(policy, transaction, getCurrency(transaction)); @@ -2608,7 +2611,7 @@ function getCategoryTaxCodeAndAmount(category: string, transaction: OnyxEntry Date: Mon, 27 Apr 2026 17:23:01 +0000 Subject: [PATCH 2/5] Rename getCategoryTaxCodeAndAmount to getCategoryTaxDetails, use existence check for taxValue, move taxValue condition above Co-authored-by: Fitsum Abebe --- src/libs/TransactionUtils/index.ts | 12 ++++++------ src/libs/actions/IOU/index.ts | 6 +++--- tests/unit/TransactionUtilsTest.ts | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 055ba115b33f..d9347248f857 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -830,13 +830,13 @@ function getUpdatedTransaction({ if (Object.hasOwn(transactionChanges, 'category') && typeof transactionChanges.category === 'string') { updatedTransaction.category = transactionChanges.category; - const {categoryTaxCode, categoryTaxAmount, categoryTaxValue} = getCategoryTaxCodeAndAmount(transactionChanges.category, transaction, policy); + const {categoryTaxCode, categoryTaxAmount, categoryTaxValue} = getCategoryTaxDetails(transactionChanges.category, transaction, policy); + if (categoryTaxValue) { + updatedTransaction.taxValue = categoryTaxValue; + } if (categoryTaxCode && categoryTaxAmount !== undefined) { updatedTransaction.taxCode = categoryTaxCode; updatedTransaction.taxAmount = categoryTaxAmount; - if (categoryTaxValue) { - updatedTransaction.taxValue = categoryTaxValue; - } } } @@ -2585,7 +2585,7 @@ function buildMergeDuplicatesParams( }; } -function getCategoryTaxCodeAndAmount(category: string, transaction: OnyxEntry, policy: OnyxEntry) { +function getCategoryTaxDetails(category: string, transaction: OnyxEntry, policy: OnyxEntry) { const taxRules = policy?.rules?.expenseRules?.filter((rule) => rule.tax); if (!taxRules || taxRules?.length === 0 || isDistanceRequest(transaction)) { return {categoryTaxCode: undefined, categoryTaxAmount: undefined, categoryTaxValue: undefined}; @@ -2907,7 +2907,7 @@ export { shouldShowAttendees, getAllSortedTransactions, getFormattedPostedDate, - getCategoryTaxCodeAndAmount, + getCategoryTaxDetails, isPerDiemRequest, isViolationDismissed, isPartialTransaction, diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 137d270df831..e9af53bc98af 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -64,7 +64,7 @@ import {startSpan} from '@libs/telemetry/activeSpans'; import { buildOptimisticTransaction, getAmount, - getCategoryTaxCodeAndAmount, + getCategoryTaxDetails, getCurrency, getDistanceInMeters, isDistanceRequest as isDistanceRequestTransactionUtils, @@ -905,9 +905,9 @@ function setMoneyRequestCategory(transactionID: string, category: string, policy return; } const transaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`]; - const {categoryTaxCode, categoryTaxAmount, categoryTaxValue} = getCategoryTaxCodeAndAmount(category, transaction, policy); + const {categoryTaxCode, categoryTaxAmount, categoryTaxValue} = getCategoryTaxDetails(category, transaction, policy); if (categoryTaxCode && categoryTaxAmount !== undefined) { - setMoneyRequestTaxRateValues(transactionID, {taxCode: categoryTaxCode, taxAmount: categoryTaxAmount, taxValue: categoryTaxValue ?? null}); + setMoneyRequestTaxRateValues(transactionID, {taxCode: categoryTaxCode, taxAmount: categoryTaxAmount, taxValue: categoryTaxValue ? categoryTaxValue : null}); } } diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index 79925928b333..ac471e81c5d5 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -192,7 +192,7 @@ describe('TransactionUtils', () => { }); }); - describe('getCategoryTaxCodeAndAmount', () => { + describe('getCategoryTaxDetails', () => { it('should return the associated tax when the category matches the tax expense rules', () => { // Given a policy with tax expense rules associated with a category const category = 'Advertising'; @@ -204,7 +204,7 @@ describe('TransactionUtils', () => { const transaction = generateTransaction(); // When retrieving the tax from the associated category - const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy); + const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxDetails(category, transaction, fakePolicy); // Then it should return the associated tax code and amount expect(categoryTaxCode).toBe('id_TAX_RATE_1'); @@ -223,7 +223,7 @@ describe('TransactionUtils', () => { const transaction = generateTransaction(); // When retrieving the tax from a category that is not associated with the tax expense rules - const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(selectedCategory, transaction, fakePolicy); + const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxDetails(selectedCategory, transaction, fakePolicy); // Then it should return the default tax code and amount expect(categoryTaxCode).toBe('id_TAX_EXEMPT'); @@ -254,7 +254,7 @@ describe('TransactionUtils', () => { const transaction = generateTransaction(); // When retrieving the tax from a category that is not associated with the tax expense rules - const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(selectedCategory, transaction, fakePolicy); + const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxDetails(selectedCategory, transaction, fakePolicy); // Then it should return the default tax code and amount expect(categoryTaxCode).toBe('id_TAX_RATE_2'); @@ -276,7 +276,7 @@ describe('TransactionUtils', () => { }; // When retrieving the tax from the associated category - const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy); + const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxDetails(category, transaction, fakePolicy); // Then it should return undefined for both the tax code and the tax amount expect(categoryTaxCode).toBe(undefined); @@ -294,7 +294,7 @@ describe('TransactionUtils', () => { const transaction = generateTransaction(); // When retrieving the tax from a category - const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy); + const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxDetails(category, transaction, fakePolicy); // Then it should return undefined for both the tax code and the tax amount expect(categoryTaxCode).toBe(undefined); From 19f109733097919b760e08806403d45176570500 Mon Sep 17 00:00:00 2001 From: "Fitsum Abebe (via MelvinBot)" Date: Mon, 27 Apr 2026 17:27:59 +0000 Subject: [PATCH 3/5] Fix no-unneeded-ternary ESLint error in IOU/index.ts Co-authored-by: Fitsum Abebe --- src/libs/actions/IOU/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index e9af53bc98af..4c072ad6271e 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -907,7 +907,7 @@ function setMoneyRequestCategory(transactionID: string, category: string, policy const transaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`]; const {categoryTaxCode, categoryTaxAmount, categoryTaxValue} = getCategoryTaxDetails(category, transaction, policy); if (categoryTaxCode && categoryTaxAmount !== undefined) { - setMoneyRequestTaxRateValues(transactionID, {taxCode: categoryTaxCode, taxAmount: categoryTaxAmount, taxValue: categoryTaxValue ? categoryTaxValue : null}); + setMoneyRequestTaxRateValues(transactionID, {taxCode: categoryTaxCode, taxAmount: categoryTaxAmount, taxValue: categoryTaxValue ?? null}); } } From 061c34186b4e281e2f5b60e9717b15fa85084236 Mon Sep 17 00:00:00 2001 From: "Fitsum Abebe (via MelvinBot)" Date: Tue, 28 Apr 2026 13:18:28 +0000 Subject: [PATCH 4/5] Address review: combine taxValue into existing condition check Move taxValue assignment into the existing categoryTaxCode/categoryTaxAmount condition block, and add categoryTaxValue to the condition itself to avoid null fallbacks. Co-authored-by: FitseTLT <38649957+FitseTLT@users.noreply.github.com> Co-authored-by: Fitsum Abebe --- src/libs/TransactionUtils/index.ts | 6 ++---- src/libs/actions/IOU/index.ts | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index d9347248f857..bb52307ac4c1 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -831,12 +831,10 @@ function getUpdatedTransaction({ if (Object.hasOwn(transactionChanges, 'category') && typeof transactionChanges.category === 'string') { updatedTransaction.category = transactionChanges.category; const {categoryTaxCode, categoryTaxAmount, categoryTaxValue} = getCategoryTaxDetails(transactionChanges.category, transaction, policy); - if (categoryTaxValue) { - updatedTransaction.taxValue = categoryTaxValue; - } - if (categoryTaxCode && categoryTaxAmount !== undefined) { + if (categoryTaxCode && categoryTaxAmount !== undefined && categoryTaxValue) { updatedTransaction.taxCode = categoryTaxCode; updatedTransaction.taxAmount = categoryTaxAmount; + updatedTransaction.taxValue = categoryTaxValue; } } diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 4c072ad6271e..07fdce8b5505 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -906,8 +906,8 @@ function setMoneyRequestCategory(transactionID: string, category: string, policy } const transaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`]; const {categoryTaxCode, categoryTaxAmount, categoryTaxValue} = getCategoryTaxDetails(category, transaction, policy); - if (categoryTaxCode && categoryTaxAmount !== undefined) { - setMoneyRequestTaxRateValues(transactionID, {taxCode: categoryTaxCode, taxAmount: categoryTaxAmount, taxValue: categoryTaxValue ?? null}); + if (categoryTaxCode && categoryTaxAmount !== undefined && categoryTaxValue) { + setMoneyRequestTaxRateValues(transactionID, {taxCode: categoryTaxCode, taxAmount: categoryTaxAmount, taxValue: categoryTaxValue}); } } From 0281443f43fd48cf4b8d77b272b8cebc85ac615d Mon Sep 17 00:00:00 2001 From: "Fitsum Abebe (via MelvinBot)" Date: Thu, 30 Apr 2026 14:19:38 +0000 Subject: [PATCH 5/5] Add categoryTaxValue assertions to getCategoryTaxDetails tests Cover the new categoryTaxValue return property in all existing getCategoryTaxDetails and getUpdatedTransaction test cases to address the Codecov coverage gap. Co-authored-by: Fitsum Abebe --- tests/unit/TransactionUtilsTest.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index cad4b580bab6..ed5fb28a0594 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -204,11 +204,12 @@ describe('TransactionUtils', () => { const transaction = generateTransaction(); // When retrieving the tax from the associated category - const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxDetails(category, transaction, fakePolicy); + const {categoryTaxCode, categoryTaxAmount, categoryTaxValue} = TransactionUtils.getCategoryTaxDetails(category, transaction, fakePolicy); - // Then it should return the associated tax code and amount + // Then it should return the associated tax code, amount, and value expect(categoryTaxCode).toBe('id_TAX_RATE_1'); expect(categoryTaxAmount).toBe(5); + expect(categoryTaxValue).toBe('5%'); }); it("should return the default tax when the category doesn't match the tax expense rules", () => { @@ -223,11 +224,12 @@ describe('TransactionUtils', () => { const transaction = generateTransaction(); // When retrieving the tax from a category that is not associated with the tax expense rules - const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxDetails(selectedCategory, transaction, fakePolicy); + const {categoryTaxCode, categoryTaxAmount, categoryTaxValue} = TransactionUtils.getCategoryTaxDetails(selectedCategory, transaction, fakePolicy); - // Then it should return the default tax code and amount + // Then it should return the default tax code, amount, and value expect(categoryTaxCode).toBe('id_TAX_EXEMPT'); expect(categoryTaxAmount).toBe(0); + expect(categoryTaxValue).toBe('0%'); }); it("should return the foreign default tax when the category doesn't match the tax expense rules and using a foreign currency", () => { @@ -254,11 +256,12 @@ describe('TransactionUtils', () => { const transaction = generateTransaction(); // When retrieving the tax from a category that is not associated with the tax expense rules - const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxDetails(selectedCategory, transaction, fakePolicy); + const {categoryTaxCode, categoryTaxAmount, categoryTaxValue} = TransactionUtils.getCategoryTaxDetails(selectedCategory, transaction, fakePolicy); - // Then it should return the default tax code and amount + // Then it should return the default tax code, amount, and value expect(categoryTaxCode).toBe('id_TAX_RATE_2'); expect(categoryTaxAmount).toBe(9); + expect(categoryTaxValue).toBe('10%'); }); describe('should return undefined tax', () => { @@ -276,11 +279,12 @@ describe('TransactionUtils', () => { }; // When retrieving the tax from the associated category - const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxDetails(category, transaction, fakePolicy); + const {categoryTaxCode, categoryTaxAmount, categoryTaxValue} = TransactionUtils.getCategoryTaxDetails(category, transaction, fakePolicy); - // Then it should return undefined for both the tax code and the tax amount + // Then it should return undefined for the tax code, amount, and value expect(categoryTaxCode).toBe(undefined); expect(categoryTaxAmount).toBe(undefined); + expect(categoryTaxValue).toBe(undefined); }); it('if there are no policy tax expense rules', () => { @@ -294,11 +298,12 @@ describe('TransactionUtils', () => { const transaction = generateTransaction(); // When retrieving the tax from a category - const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxDetails(category, transaction, fakePolicy); + const {categoryTaxCode, categoryTaxAmount, categoryTaxValue} = TransactionUtils.getCategoryTaxDetails(category, transaction, fakePolicy); - // Then it should return undefined for both the tax code and the tax amount + // Then it should return undefined for the tax code, amount, and value expect(categoryTaxCode).toBe(undefined); expect(categoryTaxAmount).toBe(undefined); + expect(categoryTaxValue).toBe(undefined); }); }); }); @@ -327,6 +332,7 @@ describe('TransactionUtils', () => { expect(updatedTransaction.category).toBe(category); expect(updatedTransaction.taxCode).toBe(taxCode); expect(updatedTransaction.taxAmount).toBe(5); + expect(updatedTransaction.taxValue).toBe('5%'); }); it('should update transaction when distance is changed', () => {