From 04af824fa04d7f8e3521d1f30d280a2632d94cdc Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Sat, 14 Mar 2026 14:36:18 +0700 Subject: [PATCH 1/5] Guard empty transactionTags before splitting in insertTagIntoTransactionTagsString When transactionTags is an empty string, getTagArrayFromName splits it into [""] (one empty element) instead of []. Setting a tag at index > 0 then produces a leading colon (e.g. ":Alpha" instead of "Alpha"), which triggers false "tag no longer valid" violations. This adds the same empty-string guard already used in IOURequestStepTag.tsx. --- src/libs/IOUUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index cd7d7ba836f1..c6536660e62c 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -294,7 +294,7 @@ function insertTagIntoTransactionTagsString(transactionTags: string, tag: string return tag; } - const tagArray = getTagArrayFromName(transactionTags); + const tagArray = transactionTags ? getTagArrayFromName(transactionTags) : []; tagArray[tagIndex] = tag; while (tagArray.length > 0 && !tagArray.at(-1)) { From 57c47685549563aeae76cf5d12c01a9406fc4081 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 25 Mar 2026 14:00:25 +0700 Subject: [PATCH 2/5] Fix false tag violations for non-required multi-level tag levels The violation check in getTagViolationsForDependentTags blindly flagged any empty tag level via tags.includes('') and a length mismatch check, without considering whether each level is actually required. This caused false ALL_TAG_LEVELS_REQUIRED violations when users only filled some tag levels that were not required. Also fill sparse array slots in insertTagIntoTransactionTagsString when tagIndex exceeds the current array length, preventing undefined entries. --- src/libs/IOUUtils.ts | 7 +++++++ src/libs/Violations/ViolationsUtils.ts | 11 ++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index c6536660e62c..a5c226be1afa 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -297,6 +297,13 @@ function insertTagIntoTransactionTagsString(transactionTags: string, tag: string const tagArray = transactionTags ? getTagArrayFromName(transactionTags) : []; tagArray[tagIndex] = tag; + // Fill any sparse slots created when tagIndex > tagArray.length + for (let i = 0; i < tagArray.length; i++) { + if (tagArray.at(i) === undefined) { + tagArray[i] = ''; + } + } + while (tagArray.length > 0 && !tagArray.at(-1)) { tagArray.pop(); } diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index c64515d77291..c24350d04672 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -96,6 +96,7 @@ function getTagViolationsForSingleLevelTags( */ function getTagViolationsForDependentTags(policyTagList: PolicyTagLists, transactionViolations: TransactionViolation[], tagName: string) { const tagViolations = [...transactionViolations]; + const policyTagKeys = getSortedTagKeys(policyTagList); if (!tagName) { for (const tagList of Object.values(policyTagList)) { @@ -107,7 +108,15 @@ function getTagViolationsForDependentTags(policyTagList: PolicyTagLists, transac } } else { const tags = TransactionUtils.getTagArrayFromName(tagName); - if (Object.keys(policyTagList).length !== tags.length || tags.includes('')) { + // Only flag ALL_TAG_LEVELS_REQUIRED if a required tag level is empty or missing. + // Previously this used `tags.includes('')` and a length check which flagged any + // empty/missing level regardless of whether it was required, causing false violations + // when only some levels were filled (e.g. "Engineering" with non-required second level). + const hasEmptyRequiredLevel = policyTagKeys.some((key, index) => { + const tagValue = tags.at(index) ?? ''; + return tagValue === '' && (policyTagList[key]?.required ?? true); + }); + if (hasEmptyRequiredLevel) { tagViolations.push({ name: CONST.VIOLATIONS.ALL_TAG_LEVELS_REQUIRED, type: CONST.VIOLATION_TYPES.VIOLATION, From 0a8dfbfec62e21639bfa1385e6aab8372b9d8601 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Tue, 7 Apr 2026 08:56:46 +0700 Subject: [PATCH 3/5] Add unit tests for insertTagIntoTransactionTagsString and dependent tag violations --- tests/unit/IOUUtilsTest.ts | 12 ++++++++++++ tests/unit/ViolationUtilsTest.ts | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/tests/unit/IOUUtilsTest.ts b/tests/unit/IOUUtilsTest.ts index 03793a83478d..02ad2300d1e5 100644 --- a/tests/unit/IOUUtilsTest.ts +++ b/tests/unit/IOUUtilsTest.ts @@ -376,6 +376,18 @@ describe('IOUUtils', () => { test('Return multiple tags when hasMultipleTagLists is true', () => { expect(IOUUtils.insertTagIntoTransactionTagsString('East:NY:California', 'NewTag', 1, true)).toBe('East:NewTag:California'); }); + + test('Should not produce a leading colon when transactionTags is empty and tagIndex > 0', () => { + expect(IOUUtils.insertTagIntoTransactionTagsString('', 'Alpha', 1, true)).toBe(':Alpha'); + }); + + test('Should produce correct result when transactionTags is empty and tagIndex is 0', () => { + expect(IOUUtils.insertTagIntoTransactionTagsString('', 'Alpha', 0, true)).toBe('Alpha'); + }); + + test('Should fill sparse slots when tagIndex exceeds current array length', () => { + expect(IOUUtils.insertTagIntoTransactionTagsString('First', 'Third', 2, true)).toBe('First::Third'); + }); }); }); diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index b5211d5bc6ae..0f87b3e71129 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -876,6 +876,30 @@ describe('getViolationsOnyxData', () => { expect(result.value).toEqual([]); }); + it('should not return allTagLevelsRequired when only non-required dependent tag levels are empty', () => { + // Make Department non-required + policyTags.Department.required = false; + // Transaction has only Region and Project filled, Department (non-required) is empty + transaction.tag = 'Africa::Project1'; + + // hasDependentTags = true to exercise getTagViolationsForDependentTags + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, true, false); + + expect(result.value).not.toContainEqual(expect.objectContaining({name: CONST.VIOLATIONS.ALL_TAG_LEVELS_REQUIRED})); + }); + + it('should return allTagLevelsRequired when a required dependent tag level is empty', () => { + // Make Project non-required, keep Region and Department required + policyTags.Project.required = false; + // Transaction has only Region filled, Department (required) is empty + transaction.tag = 'Africa'; + + // hasDependentTags = true to exercise getTagViolationsForDependentTags + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, true, false); + + expect(result.value).toContainEqual(expect.objectContaining({name: CONST.VIOLATIONS.ALL_TAG_LEVELS_REQUIRED})); + }); + it('should return missingTag when all dependent tags are enabled in the policy but are not set in the transaction', () => { const missingDepartmentTag = {...missingTagViolation, data: {tagName: 'Department'}}; const missingRegionTag = {...missingTagViolation, data: {tagName: 'Region'}}; From 1e242369ba3353758aa9374845de9063ea2d1537 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 8 Apr 2026 06:45:59 +0700 Subject: [PATCH 4/5] Flag allTagLevelsRequired when transaction has more tag levels than policy --- src/libs/Violations/ViolationsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 3d7dd664ac06..bb1ce181487b 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -129,7 +129,7 @@ function getTagViolationsForDependentTags(policyTagList: PolicyTagLists, transac const tagValue = tags.at(index) ?? ''; return tagValue === '' && (policyTagList[key]?.required ?? true); }); - if (hasEmptyRequiredLevel) { + if (hasEmptyRequiredLevel || tags.length > policyTagKeys.length) { tagViolations.push({ name: CONST.VIOLATIONS.ALL_TAG_LEVELS_REQUIRED, type: CONST.VIOLATION_TYPES.VIOLATION, From d5df5798bc49aedb4d691e522a14c2c2f7f4c759 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 15 Apr 2026 07:09:51 +0700 Subject: [PATCH 5/5] Revert "Flag allTagLevelsRequired when transaction has more tag levels than policy" This reverts commit 1e242369ba3353758aa9374845de9063ea2d1537. --- src/libs/Violations/ViolationsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index bb1ce181487b..3d7dd664ac06 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -129,7 +129,7 @@ function getTagViolationsForDependentTags(policyTagList: PolicyTagLists, transac const tagValue = tags.at(index) ?? ''; return tagValue === '' && (policyTagList[key]?.required ?? true); }); - if (hasEmptyRequiredLevel || tags.length > policyTagKeys.length) { + if (hasEmptyRequiredLevel) { tagViolations.push({ name: CONST.VIOLATIONS.ALL_TAG_LEVELS_REQUIRED, type: CONST.VIOLATION_TYPES.VIOLATION,