From 01281edf56449db7b6d6375b93214f3df36e8065 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Wed, 28 Feb 2024 15:38:59 -0800 Subject: [PATCH 01/18] Add violation logic for multi level tags --- .../ReportActionItem/MoneyRequestView.tsx | 16 +++++++------- src/hooks/useViolations.ts | 21 ++++++++++++++++++- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/libs/Violations/ViolationsUtils.ts | 2 +- 5 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 0af7140e1523..c8556374aa43 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -145,7 +145,10 @@ function MoneyRequestView({ const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true)); const {getViolationsForField} = useViolations(transactionViolations ?? []); - const hasViolations = useCallback((field: ViolationField): boolean => !!canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]); + const hasViolations = useCallback( + (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']): boolean => !!canUseViolations && getViolationsForField(field, data).length > 0, + [canUseViolations, getViolationsForField], + ); let amountDescription = `${translate('iou.amount')}`; @@ -198,7 +201,7 @@ function MoneyRequestView({ const getPendingFieldAction = (fieldPath: TransactionPendingFieldsKey) => transaction?.pendingFields?.[fieldPath] ?? pendingAction; const getErrorForField = useCallback( - (field: ViolationField, data?: OnyxTypes.TransactionViolation['data'], shouldShowViolations = true) => { + (field: ViolationField, data?: OnyxTypes.TransactionViolation['data']) => { // Checks applied when creating a new money request // NOTE: receipt field can return multiple violations, so we need to handle it separately const fieldChecks: Partial> = { @@ -224,9 +227,8 @@ function MoneyRequestView({ } // Return violations if there are any - // At the moment, we only return violations for tags for workspaces with single-level tags - if (canUseViolations && shouldShowViolations && hasViolations(field)) { - const violations = getViolationsForField(field); + if (canUseViolations && hasViolations(field, data)) { + const violations = getViolationsForField(field, data); return ViolationsUtils.getViolationTranslation(violations[0], translate); } @@ -397,8 +399,8 @@ function MoneyRequestView({ ROUTES.MONEY_REQUEST_STEP_TAG.getRoute(CONST.IOU.ACTION.EDIT, CONST.IOU.TYPE.REQUEST, index, transaction?.transactionID ?? '', report.reportID), ) } - brickRoadIndicator={getErrorForField('tag', {}, policyTagLists.length === 1) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - error={getErrorForField('tag', {}, policyTagLists.length === 1)} + brickRoadIndicator={getErrorForField('tag', {tagListIndex: index, tagListName: name}) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + error={getErrorForField('tag', {tagListIndex: index, tagListName: name})} /> ))} diff --git a/src/hooks/useViolations.ts b/src/hooks/useViolations.ts index 29b2dcb86718..a422f75714b5 100644 --- a/src/hooks/useViolations.ts +++ b/src/hooks/useViolations.ts @@ -58,7 +58,26 @@ function useViolations(violations: TransactionViolation[]) { } return violationGroups ?? new Map(); }, [violations]); - const getViolationsForField = useCallback((field: ViolationField) => violationsByField.get(field) ?? [], [violationsByField]); + + const getViolationsForField = useCallback( + (field: ViolationField, data?: TransactionViolation['data']) => { + const currentViolations = violationsByField.get(field) ?? []; + if (_.isNumber(data?.tagListIndex) && currentViolations[0]?.name === 'someTagLevelsRequired' && Array.isArray(currentViolations[0]?.data?.errorIndexes)) { + return currentViolations + .filter((violation) => violation.data?.errorIndexes?.includes(data?.tagListIndex)) + .map((violation) => ({ + ...violation, + data: { + ...violation.data, + tagName: data?.tagListName, + }, + })); + } + + return currentViolations; + }, + [violationsByField], + ); return { getViolationsForField, diff --git a/src/languages/en.ts b/src/languages/en.ts index 4fa22d8e255d..647c533620f1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2363,7 +2363,7 @@ export default { return ''; }, smartscanFailed: 'Receipt scanning failed. Enter details manually.', - someTagLevelsRequired: 'Missing tag', + someTagLevelsRequired: ({tagName}: ViolationsTagOutOfPolicyParams) => `Missing ${tagName ?? 'Tag'}`, tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `${tagName ?? 'Tag'} no longer valid`, taxAmountChanged: 'Tax amount was modified', taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? 'Tax'} no longer valid`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 7bea48d71d36..fc5de7356ff3 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2855,7 +2855,7 @@ export default { return ''; }, smartscanFailed: 'No se pudo escanear el recibo. Introduce los datos manualmente', - someTagLevelsRequired: 'Falta etiqueta', + someTagLevelsRequired: ({tagName}: ViolationsTagOutOfPolicyParams) => `Falta ${tagName ?? 'Tag'}`, tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `La etiqueta ${tagName ? `${tagName} ` : ''}ya no es válida`, taxAmountChanged: 'El importe del impuesto fue modificado', taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? 'El impuesto'} ya no es válido`, diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 6153ea62cd0d..2671716fbfba 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -181,7 +181,7 @@ const ViolationsUtils = { case 'smartscanFailed': return translate('violations.smartscanFailed'); case 'someTagLevelsRequired': - return translate('violations.someTagLevelsRequired'); + return translate('violations.someTagLevelsRequired', {tagName}); case 'tagOutOfPolicy': return translate('violations.tagOutOfPolicy', {tagName}); case 'taxAmountChanged': From f0093d6190c97c52f24da98ffa6dc26994942094 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Wed, 28 Feb 2024 15:44:53 -0800 Subject: [PATCH 02/18] Lint + TS --- src/hooks/useViolations.ts | 6 ++++-- src/types/onyx/TransactionViolation.ts | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/hooks/useViolations.ts b/src/hooks/useViolations.ts index a422f75714b5..6c975c7e1824 100644 --- a/src/hooks/useViolations.ts +++ b/src/hooks/useViolations.ts @@ -62,9 +62,11 @@ function useViolations(violations: TransactionViolation[]) { const getViolationsForField = useCallback( (field: ViolationField, data?: TransactionViolation['data']) => { const currentViolations = violationsByField.get(field) ?? []; - if (_.isNumber(data?.tagListIndex) && currentViolations[0]?.name === 'someTagLevelsRequired' && Array.isArray(currentViolations[0]?.data?.errorIndexes)) { + + // tagListIndex can be 0 so we compare with undefined + if (data?.tagListIndex !== undefined && currentViolations[0]?.name === 'someTagLevelsRequired' && Array.isArray(currentViolations[0]?.data?.errorIndexes)) { return currentViolations - .filter((violation) => violation.data?.errorIndexes?.includes(data?.tagListIndex)) + .filter((violation) => violation.data?.errorIndexes?.includes(data?.tagListIndex ?? -1)) .map((violation) => ({ ...violation, data: { diff --git a/src/types/onyx/TransactionViolation.ts b/src/types/onyx/TransactionViolation.ts index 9133eca63c65..28de4582bd5e 100644 --- a/src/types/onyx/TransactionViolation.ts +++ b/src/types/onyx/TransactionViolation.ts @@ -26,6 +26,9 @@ type TransactionViolation = { isTransactionOlderThan7Days?: boolean; member?: string; taxName?: string; + tagListIndex?: number; + tagListName?: string; + errorIndexes?: number[]; }; }; From 7520f366c1d7ceaceae607b1d3cb66b817c18bd6 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Wed, 28 Feb 2024 15:56:39 -0800 Subject: [PATCH 03/18] Check tagListIndex only for someTagLevelsRequired --- src/hooks/useViolations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useViolations.ts b/src/hooks/useViolations.ts index 6c975c7e1824..482fab1a2988 100644 --- a/src/hooks/useViolations.ts +++ b/src/hooks/useViolations.ts @@ -64,7 +64,7 @@ function useViolations(violations: TransactionViolation[]) { const currentViolations = violationsByField.get(field) ?? []; // tagListIndex can be 0 so we compare with undefined - if (data?.tagListIndex !== undefined && currentViolations[0]?.name === 'someTagLevelsRequired' && Array.isArray(currentViolations[0]?.data?.errorIndexes)) { + if (currentViolations[0]?.name === 'someTagLevelsRequired' && data?.tagListIndex !== undefined && Array.isArray(currentViolations[0]?.data?.errorIndexes)) { return currentViolations .filter((violation) => violation.data?.errorIndexes?.includes(data?.tagListIndex ?? -1)) .map((violation) => ({ From b17a7c9e17cc8310a362b7788e005274458779e5 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Wed, 28 Feb 2024 17:13:53 -0800 Subject: [PATCH 04/18] Add someTagLevelsRequired violation on the client --- src/hooks/useViolations.ts | 6 ++++++ src/libs/Violations/ViolationsUtils.ts | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/hooks/useViolations.ts b/src/hooks/useViolations.ts index 482fab1a2988..3df457f1205a 100644 --- a/src/hooks/useViolations.ts +++ b/src/hooks/useViolations.ts @@ -63,6 +63,7 @@ function useViolations(violations: TransactionViolation[]) { (field: ViolationField, data?: TransactionViolation['data']) => { const currentViolations = violationsByField.get(field) ?? []; + // someTagLevelsRequired has special logic becase data.errorIndexes is a bit unique in how it denotes the tag list that has the violation // tagListIndex can be 0 so we compare with undefined if (currentViolations[0]?.name === 'someTagLevelsRequired' && data?.tagListIndex !== undefined && Array.isArray(currentViolations[0]?.data?.errorIndexes)) { return currentViolations @@ -76,6 +77,11 @@ function useViolations(violations: TransactionViolation[]) { })); } + // tagOutOfPolicy has special logic because we have to account for multi-level tags and use tagName to find the right tag to put the violation on + if (currentViolations[0]?.name === 'tagOutOfPolicy' && data?.tagListName !== undefined && currentViolations[0]?.data?.tagName) { + return currentViolations.filter((violation) => violation.data?.tagName === data?.tagListName); + } + return currentViolations; }, [violationsByField], diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 2671716fbfba..007c4fd78fb8 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -50,6 +50,7 @@ const ViolationsUtils = { } if (policyRequiresTags) { + const selectedTags = updatedTransaction.tag?.split(CONST.COLON) ?? []; const policyTagKeys = Object.keys(policyTagList); // At the moment, we only return violations for tags for workspaces with single-level tags @@ -78,6 +79,24 @@ const ViolationsUtils = { if (!hasMissingTagViolation && !updatedTransaction.tag && policyRequiresTags) { newTransactionViolations.push({name: CONST.VIOLATIONS.MISSING_TAG, type: 'violation'}); } + } else { + let errorIndexes = []; + for (let i = 0; i < policyTagKeys.length; i++) { + const isTagRequired = policyTagList[policyTagKeys[i]].required ?? true; + const isTagSelected = Boolean(selectedTags[i]); + if (isTagRequired && (!isTagSelected || (selectedTags.length === 1 && selectedTags[0] === ''))){ + errorIndexes.push(i) + } + } + if (errorIndexes.length) { + newTransactionViolations.push({ + name: CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED, + type: 'violation', + data: { + errorIndexes, + }, + }); + } } } From bf609e745f44d6ad45176715efe4d86257d28175 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Wed, 28 Feb 2024 17:51:34 -0800 Subject: [PATCH 05/18] Multi tag violations on the client --- src/libs/Violations/ViolationsUtils.ts | 42 +++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 007c4fd78fb8..f6765083db50 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -6,6 +6,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation} from '@src/types/onyx'; +import _ from 'underscore'; const ViolationsUtils = { /** @@ -80,15 +81,27 @@ const ViolationsUtils = { newTransactionViolations.push({name: CONST.VIOLATIONS.MISSING_TAG, type: 'violation'}); } } else { + newTransactionViolations = reject(newTransactionViolations, { + name: CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED, + }); + newTransactionViolations = reject(newTransactionViolations, { + name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, + }); + + // calculate errorIndexes for someTagLevelsRequired + // if it's empty, reject it from current violations + // else push it to onyx let errorIndexes = []; for (let i = 0; i < policyTagKeys.length; i++) { const isTagRequired = policyTagList[policyTagKeys[i]].required ?? true; const isTagSelected = Boolean(selectedTags[i]); + // console.log('i', i, isTagRequired, isTagSelected, selectedTags, policyTagList[policyTagKeys[i]], updatedTransaction.tag); + // console.log('i', i, isTagRequired, isTagSelected, selectedTags); if (isTagRequired && (!isTagSelected || (selectedTags.length === 1 && selectedTags[0] === ''))){ errorIndexes.push(i) } } - if (errorIndexes.length) { + if (errorIndexes.length !== 0) { newTransactionViolations.push({ name: CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED, type: 'violation', @@ -96,6 +109,33 @@ const ViolationsUtils = { errorIndexes, }, }); + } else { + let hasInvalidTag = false; + for (let i = 0; i < policyTagKeys.length; i++) { + const isTagRequired = policyTagList[policyTagKeys[i]].required ?? true; + const selectedTag = selectedTags[i]; + const tags = policyTagList[policyTagKeys[i]].tags; + const isTagInPolicy = _.some(tags, (tag, tagKey) => tag.name === selectedTag && Boolean(tag.enabled)); + // console.log(policyTagList[policyTagKeys[i]]); + // console.log('i', i, isTagRequired, isTagSelected, selectedTags, policyTagList[policyTagKeys[i]], updatedTransaction.tag); + // console.log('i', i, isTagRequired, isTagInPolicy, tags, selectedTag, selectedTags, policyTagKeys[i]); + if (!isTagInPolicy) { + newTransactionViolations.push({ + name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, + type: 'violation', + data: { + tagName: policyTagKeys[i], + }, + }); + hasInvalidTag = true; + break + } + } + if (!hasInvalidTag) { + newTransactionViolations = reject(newTransactionViolations, { + name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, + }); + } } } } From 5c1a77e8fa137c78b98e54a7179396df0efd4e5b Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Thu, 29 Feb 2024 15:42:27 -0800 Subject: [PATCH 06/18] Cleanup --- src/libs/Violations/ViolationsUtils.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index f6765083db50..ed92b652d16a 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -95,8 +95,6 @@ const ViolationsUtils = { for (let i = 0; i < policyTagKeys.length; i++) { const isTagRequired = policyTagList[policyTagKeys[i]].required ?? true; const isTagSelected = Boolean(selectedTags[i]); - // console.log('i', i, isTagRequired, isTagSelected, selectedTags, policyTagList[policyTagKeys[i]], updatedTransaction.tag); - // console.log('i', i, isTagRequired, isTagSelected, selectedTags); if (isTagRequired && (!isTagSelected || (selectedTags.length === 1 && selectedTags[0] === ''))){ errorIndexes.push(i) } @@ -116,9 +114,6 @@ const ViolationsUtils = { const selectedTag = selectedTags[i]; const tags = policyTagList[policyTagKeys[i]].tags; const isTagInPolicy = _.some(tags, (tag, tagKey) => tag.name === selectedTag && Boolean(tag.enabled)); - // console.log(policyTagList[policyTagKeys[i]]); - // console.log('i', i, isTagRequired, isTagSelected, selectedTags, policyTagList[policyTagKeys[i]], updatedTransaction.tag); - // console.log('i', i, isTagRequired, isTagInPolicy, tags, selectedTag, selectedTags, policyTagKeys[i]); if (!isTagInPolicy) { newTransactionViolations.push({ name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, @@ -128,7 +123,7 @@ const ViolationsUtils = { }, }); hasInvalidTag = true; - break + break; } } if (!hasInvalidTag) { From 3a36c34ec350ed2ed4b7861a436b3b1ddf4071da Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 1 Mar 2024 13:21:41 -0800 Subject: [PATCH 07/18] Remove bad test data --- tests/unit/ViolationUtilsTest.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/unit/ViolationUtilsTest.js b/tests/unit/ViolationUtilsTest.js index ff86b5fc6753..6e86dee8b31c 100644 --- a/tests/unit/ViolationUtilsTest.js +++ b/tests/unit/ViolationUtilsTest.js @@ -132,11 +132,6 @@ describe('getViolationsOnyxData', () => { Lunch: {name: 'Lunch', enabled: true}, Dinner: {name: 'Dinner', enabled: true}, }, - Tag: { - name: 'Tag', - required: true, - tags: {Lunch: {enabled: true}, Dinner: {enabled: true}}, - }, }, }; transaction.tag = 'Lunch'; From 38d00514710e52adf40af551d25b0cde780bfc1a Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 1 Mar 2024 13:37:15 -0800 Subject: [PATCH 08/18] Add tests for someTagLevelsRequired --- tests/unit/ViolationUtilsTest.js | 70 ++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/unit/ViolationUtilsTest.js b/tests/unit/ViolationUtilsTest.js index 6e86dee8b31c..bd25b1fdc978 100644 --- a/tests/unit/ViolationUtilsTest.js +++ b/tests/unit/ViolationUtilsTest.js @@ -196,4 +196,74 @@ describe('getViolationsOnyxData', () => { expect(result.value).not.toContainEqual([missingTagViolation]); }); }); + describe('policy has multi level tags', () => { + beforeEach(() => { + policyRequiresTags = true; + policyTags = { + Department: { + name: "Department", + tags: { + Accounting: { + name: "Accounting", + enabled: true + }, + Engineering: { + name: "Engineering", + enabled: false + }, + }, + required: true + }, + Region: { + name: "Region", + tags: { + Africa: { + name: "Africa", + enabled: true + }, + }, + }, + Project: { + name: "Project", + tags: { + Project1: { + name: "Project1", + enabled: true + }, + }, + required: true + } + }; + }); + it('should return someTagLevelsRequired when a required tag is missing', () => { + let someTagLevelsRequiredViolation = { + name: 'someTagLevelsRequired', + type: 'violation', + data: { + errorIndexes: [0, 1, 2], + }, + }; + + // Test case where transaction has no tags + let result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + expect(result.value).toEqual([someTagLevelsRequiredViolation]); + + // Test case where transaction has 1 tag + transaction.tag = 'Accounting'; + someTagLevelsRequiredViolation.data = {errorIndexes: [1, 2]}; + result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + expect(result.value).toEqual([someTagLevelsRequiredViolation]); + + // Test case where transaction has 2 tags + transaction.tag = 'Accounting::Project1'; + someTagLevelsRequiredViolation.data = {errorIndexes: [1]}; + result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + expect(result.value).toEqual([someTagLevelsRequiredViolation]); + + // Test case where transaction has all tags + transaction.tag = 'Accounting:Africa:Project1'; + result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + expect(result.value).toEqual([]); + }); + }); }); From 24025375eb3674320612829d5daeabc9c5b9513e Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 1 Mar 2024 13:41:38 -0800 Subject: [PATCH 09/18] Add tagOutOfPolicyViolation testfor multi tags --- tests/unit/ViolationUtilsTest.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/unit/ViolationUtilsTest.js b/tests/unit/ViolationUtilsTest.js index bd25b1fdc978..1e341954e1ac 100644 --- a/tests/unit/ViolationUtilsTest.js +++ b/tests/unit/ViolationUtilsTest.js @@ -265,5 +265,12 @@ describe('getViolationsOnyxData', () => { result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); expect(result.value).toEqual([]); }); + it('should return tagOutOfPolicy when a tag is not enabled in the policy but is set in the transaction', () => { + policyTags.Department.tags.Accounting.enabled = false; + transaction.tag = 'Accounting:Africa:Project1'; + result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const violation = {...tagOutOfPolicyViolation, data: {tagName: 'Department'}}; + expect(result.value).toEqual([violation]); + }); }); }); From 983e245985eb5e047106829cd94580bb52f9e07c Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 1 Mar 2024 15:59:43 -0800 Subject: [PATCH 10/18] Lint + Style --- src/libs/Violations/ViolationsUtils.ts | 10 ++++---- tests/unit/ViolationUtilsTest.js | 32 +++++++++++++------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index ed92b652d16a..4e904b387553 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -6,7 +6,6 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation} from '@src/types/onyx'; -import _ from 'underscore'; const ViolationsUtils = { /** @@ -91,12 +90,12 @@ const ViolationsUtils = { // calculate errorIndexes for someTagLevelsRequired // if it's empty, reject it from current violations // else push it to onyx - let errorIndexes = []; + const errorIndexes = []; for (let i = 0; i < policyTagKeys.length; i++) { const isTagRequired = policyTagList[policyTagKeys[i]].required ?? true; const isTagSelected = Boolean(selectedTags[i]); - if (isTagRequired && (!isTagSelected || (selectedTags.length === 1 && selectedTags[0] === ''))){ - errorIndexes.push(i) + if (isTagRequired && (!isTagSelected || (selectedTags.length === 1 && selectedTags[0] === ''))) { + errorIndexes.push(i); } } if (errorIndexes.length !== 0) { @@ -110,10 +109,9 @@ const ViolationsUtils = { } else { let hasInvalidTag = false; for (let i = 0; i < policyTagKeys.length; i++) { - const isTagRequired = policyTagList[policyTagKeys[i]].required ?? true; const selectedTag = selectedTags[i]; const tags = policyTagList[policyTagKeys[i]].tags; - const isTagInPolicy = _.some(tags, (tag, tagKey) => tag.name === selectedTag && Boolean(tag.enabled)); + const isTagInPolicy = Object.values(tags).some((tag) => tag.name === selectedTag && Boolean(tag.enabled)); if (!isTagInPolicy) { newTransactionViolations.push({ name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, diff --git a/tests/unit/ViolationUtilsTest.js b/tests/unit/ViolationUtilsTest.js index 1e341954e1ac..634108ecbcc7 100644 --- a/tests/unit/ViolationUtilsTest.js +++ b/tests/unit/ViolationUtilsTest.js @@ -201,42 +201,42 @@ describe('getViolationsOnyxData', () => { policyRequiresTags = true; policyTags = { Department: { - name: "Department", + name: 'Department', tags: { Accounting: { - name: "Accounting", - enabled: true + name: 'Accounting', + enabled: true, }, Engineering: { - name: "Engineering", - enabled: false + name: 'Engineering', + enabled: false, }, }, - required: true + required: true, }, Region: { - name: "Region", + name: 'Region', tags: { Africa: { - name: "Africa", - enabled: true + name: 'Africa', + enabled: true, }, }, }, Project: { - name: "Project", + name: 'Project', tags: { Project1: { - name: "Project1", - enabled: true + name: 'Project1', + enabled: true, }, }, - required: true - } + required: true, + }, }; }); it('should return someTagLevelsRequired when a required tag is missing', () => { - let someTagLevelsRequiredViolation = { + const someTagLevelsRequiredViolation = { name: 'someTagLevelsRequired', type: 'violation', data: { @@ -268,7 +268,7 @@ describe('getViolationsOnyxData', () => { it('should return tagOutOfPolicy when a tag is not enabled in the policy but is set in the transaction', () => { policyTags.Department.tags.Accounting.enabled = false; transaction.tag = 'Accounting:Africa:Project1'; - result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); + const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policyRequiresTags, policyTags, policyRequiresCategories, policyCategories); const violation = {...tagOutOfPolicyViolation, data: {tagName: 'Department'}}; expect(result.value).toEqual([violation]); }); From 6b4de30c8566f0e4782a4107c9c92f50ffe7b6a9 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 1 Mar 2024 16:03:24 -0800 Subject: [PATCH 11/18] Remove unnecessary tag in test --- tests/unit/ViolationUtilsTest.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/unit/ViolationUtilsTest.js b/tests/unit/ViolationUtilsTest.js index 634108ecbcc7..15a3a4f7de07 100644 --- a/tests/unit/ViolationUtilsTest.js +++ b/tests/unit/ViolationUtilsTest.js @@ -207,10 +207,6 @@ describe('getViolationsOnyxData', () => { name: 'Accounting', enabled: true, }, - Engineering: { - name: 'Engineering', - enabled: false, - }, }, required: true, }, From 5c4f7c3ed85ccd0206c318dc9b35dd5d53ea6506 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Wed, 6 Mar 2024 15:47:41 -0800 Subject: [PATCH 12/18] Use filter instead of 2 rejects --- src/libs/Violations/ViolationsUtils.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 4e904b387553..ebc2f037f10f 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -80,12 +80,7 @@ const ViolationsUtils = { newTransactionViolations.push({name: CONST.VIOLATIONS.MISSING_TAG, type: 'violation'}); } } else { - newTransactionViolations = reject(newTransactionViolations, { - name: CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED, - }); - newTransactionViolations = reject(newTransactionViolations, { - name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, - }); + newTransactionViolations = newTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED && violation.name !== CONST.VIOLATIONS.TAG_OUT_OF_POLICY); // calculate errorIndexes for someTagLevelsRequired // if it's empty, reject it from current violations From e12c90650b3cc31d300c801ce4738a8a24147fde Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Wed, 6 Mar 2024 15:48:03 -0800 Subject: [PATCH 13/18] Improve comment --- src/libs/Violations/ViolationsUtils.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index ebc2f037f10f..20e8a8187f64 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -82,9 +82,8 @@ const ViolationsUtils = { } else { newTransactionViolations = newTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED && violation.name !== CONST.VIOLATIONS.TAG_OUT_OF_POLICY); - // calculate errorIndexes for someTagLevelsRequired - // if it's empty, reject it from current violations - // else push it to onyx + // We first get the errorIndexes for someTagLevelsRequired. If it's not empty, we puth SOME_TAG_LEVELS_REQUIRED in Onyx. + // Otherwise, we put TAG_OUT_OF_POLICY in Onyx (when applicable) const errorIndexes = []; for (let i = 0; i < policyTagKeys.length; i++) { const isTagRequired = policyTagList[policyTagKeys[i]].required ?? true; From 20889e5168dab6f20bb83db44c27b3792bda2545 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Wed, 6 Mar 2024 17:16:55 -0800 Subject: [PATCH 14/18] Prettier --- src/libs/Violations/ViolationsUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 20e8a8187f64..08008aef1b12 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -80,7 +80,9 @@ const ViolationsUtils = { newTransactionViolations.push({name: CONST.VIOLATIONS.MISSING_TAG, type: 'violation'}); } } else { - newTransactionViolations = newTransactionViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED && violation.name !== CONST.VIOLATIONS.TAG_OUT_OF_POLICY); + newTransactionViolations = newTransactionViolations.filter( + (violation) => violation.name !== CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED && violation.name !== CONST.VIOLATIONS.TAG_OUT_OF_POLICY, + ); // We first get the errorIndexes for someTagLevelsRequired. If it's not empty, we puth SOME_TAG_LEVELS_REQUIRED in Onyx. // Otherwise, we put TAG_OUT_OF_POLICY in Onyx (when applicable) From a47a77cf9e58fd75081a1795c4e32c7ab9a45eb6 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 8 Mar 2024 10:06:21 -0800 Subject: [PATCH 15/18] Remove outdated comment --- src/libs/Violations/ViolationsUtils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 08008aef1b12..2374f55b9283 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -52,8 +52,6 @@ const ViolationsUtils = { if (policyRequiresTags) { const selectedTags = updatedTransaction.tag?.split(CONST.COLON) ?? []; const policyTagKeys = Object.keys(policyTagList); - - // At the moment, we only return violations for tags for workspaces with single-level tags if (policyTagKeys.length === 1) { const policyTagListName = policyTagKeys[0]; const policyTags = policyTagList[policyTagListName]?.tags; From ff3bebbf7d3320a0497cb50af70c5802ad5762c4 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 8 Mar 2024 10:46:38 -0800 Subject: [PATCH 16/18] New getTagViolationsForSingleLevelTags --- src/libs/Violations/ViolationsUtils.ts | 65 ++++++++++++++++---------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 2374f55b9283..c23c07bde053 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -7,6 +7,46 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyCategories, PolicyTagList, Transaction, TransactionViolation} from '@src/types/onyx'; +/** + * Calculates tag out of policy and missing tag violations for the given transaction + */ +function getTagViolationsForSingleLevelTags( + updatedTransaction: Transaction, + transactionViolations: TransactionViolation[], + policyRequiresTags: boolean, + policyTagList: PolicyTagList, +): TransactionViolation[] { + const policyTagKeys = Object.keys(policyTagList); + const policyTagListName = policyTagKeys[0]; + const policyTags = policyTagList[policyTagListName]?.tags; + const hasTagOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.TAG_OUT_OF_POLICY); + let newTransactionViolations = [...transactionViolations]; + const hasMissingTagViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.MISSING_TAG); + const isTagInPolicy = policyTags ? !!policyTags[updatedTransaction.tag ?? '']?.enabled : false; + + // Add 'tagOutOfPolicy' violation if tag is not in policy + if (!hasTagOutOfPolicyViolation && updatedTransaction.tag && !isTagInPolicy) { + newTransactionViolations.push({name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, type: 'violation'}); + } + + // Remove 'tagOutOfPolicy' violation if tag is in policy + if (hasTagOutOfPolicyViolation && updatedTransaction.tag && isTagInPolicy) { + newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY}); + } + + // Remove 'missingTag' violation if tag is valid according to policy + if (hasMissingTagViolation && isTagInPolicy) { + newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.MISSING_TAG}); + } + + // Add 'missingTag violation' if tag is required and not set + if (!hasMissingTagViolation && !updatedTransaction.tag && policyRequiresTags) { + newTransactionViolations.push({name: CONST.VIOLATIONS.MISSING_TAG, type: 'violation'}); + } + return newTransactionViolations; +}; + + const ViolationsUtils = { /** * Checks a transaction for policy violations and returns an object with Onyx method, key and updated transaction @@ -53,30 +93,7 @@ const ViolationsUtils = { const selectedTags = updatedTransaction.tag?.split(CONST.COLON) ?? []; const policyTagKeys = Object.keys(policyTagList); if (policyTagKeys.length === 1) { - const policyTagListName = policyTagKeys[0]; - const policyTags = policyTagList[policyTagListName]?.tags; - const hasTagOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.TAG_OUT_OF_POLICY); - const hasMissingTagViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.MISSING_TAG); - const isTagInPolicy = policyTags ? !!policyTags[updatedTransaction.tag ?? '']?.enabled : false; - - // Add 'tagOutOfPolicy' violation if tag is not in policy - if (!hasTagOutOfPolicyViolation && updatedTransaction.tag && !isTagInPolicy) { - newTransactionViolations.push({name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, type: 'violation'}); - } - - // Remove 'tagOutOfPolicy' violation if tag is in policy - if (hasTagOutOfPolicyViolation && updatedTransaction.tag && isTagInPolicy) { - newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY}); - } - - // Remove 'missingTag' violation if tag is valid according to policy - if (hasMissingTagViolation && isTagInPolicy) { - newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.MISSING_TAG}); - } - // Add 'missingTag violation' if tag is required and not set - if (!hasMissingTagViolation && !updatedTransaction.tag && policyRequiresTags) { - newTransactionViolations.push({name: CONST.VIOLATIONS.MISSING_TAG, type: 'violation'}); - } + newTransactionViolations = getTagViolationsForSingleLevelTags(updatedTransaction, newTransactionViolations, policyRequiresTags, policyTagList); } else { newTransactionViolations = newTransactionViolations.filter( (violation) => violation.name !== CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED && violation.name !== CONST.VIOLATIONS.TAG_OUT_OF_POLICY, From 32e20ed76c6b631fc95445c4879cbe746caecbfc Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 8 Mar 2024 10:52:47 -0800 Subject: [PATCH 17/18] New getTagViolationsForMultiLevelTags --- src/libs/Violations/ViolationsUtils.ts | 109 ++++++++++++++----------- 1 file changed, 62 insertions(+), 47 deletions(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index c23c07bde053..f17210cafb65 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -46,6 +46,67 @@ function getTagViolationsForSingleLevelTags( return newTransactionViolations; }; +/** + * Calculates some tag levels required and missing tag violations for the given transaction + */ +function getTagViolationsForMultiLevelTags( + updatedTransaction: Transaction, + transactionViolations: TransactionViolation[], + policyRequiresTags: boolean, + policyTagList: PolicyTagList, +): TransactionViolation[] { + const policyTagKeys = Object.keys(policyTagList); + const selectedTags = updatedTransaction.tag?.split(CONST.COLON) ?? []; + let newTransactionViolations = [...transactionViolations]; + newTransactionViolations = newTransactionViolations.filter( + (violation) => violation.name !== CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED && violation.name !== CONST.VIOLATIONS.TAG_OUT_OF_POLICY, + ); + + // We first get the errorIndexes for someTagLevelsRequired. If it's not empty, we puth SOME_TAG_LEVELS_REQUIRED in Onyx. + // Otherwise, we put TAG_OUT_OF_POLICY in Onyx (when applicable) + const errorIndexes = []; + for (let i = 0; i < policyTagKeys.length; i++) { + const isTagRequired = policyTagList[policyTagKeys[i]].required ?? true; + const isTagSelected = Boolean(selectedTags[i]); + if (isTagRequired && (!isTagSelected || (selectedTags.length === 1 && selectedTags[0] === ''))) { + errorIndexes.push(i); + } + } + if (errorIndexes.length !== 0) { + newTransactionViolations.push({ + name: CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED, + type: 'violation', + data: { + errorIndexes, + }, + }); + } else { + let hasInvalidTag = false; + for (let i = 0; i < policyTagKeys.length; i++) { + const selectedTag = selectedTags[i]; + const tags = policyTagList[policyTagKeys[i]].tags; + const isTagInPolicy = Object.values(tags).some((tag) => tag.name === selectedTag && Boolean(tag.enabled)); + if (!isTagInPolicy) { + newTransactionViolations.push({ + name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, + type: 'violation', + data: { + tagName: policyTagKeys[i], + }, + }); + hasInvalidTag = true; + break; + } + } + if (!hasInvalidTag) { + newTransactionViolations = reject(newTransactionViolations, { + name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, + }); + } + } + return newTransactionViolations; +}; + const ViolationsUtils = { /** @@ -90,57 +151,11 @@ const ViolationsUtils = { } if (policyRequiresTags) { - const selectedTags = updatedTransaction.tag?.split(CONST.COLON) ?? []; const policyTagKeys = Object.keys(policyTagList); if (policyTagKeys.length === 1) { newTransactionViolations = getTagViolationsForSingleLevelTags(updatedTransaction, newTransactionViolations, policyRequiresTags, policyTagList); } else { - newTransactionViolations = newTransactionViolations.filter( - (violation) => violation.name !== CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED && violation.name !== CONST.VIOLATIONS.TAG_OUT_OF_POLICY, - ); - - // We first get the errorIndexes for someTagLevelsRequired. If it's not empty, we puth SOME_TAG_LEVELS_REQUIRED in Onyx. - // Otherwise, we put TAG_OUT_OF_POLICY in Onyx (when applicable) - const errorIndexes = []; - for (let i = 0; i < policyTagKeys.length; i++) { - const isTagRequired = policyTagList[policyTagKeys[i]].required ?? true; - const isTagSelected = Boolean(selectedTags[i]); - if (isTagRequired && (!isTagSelected || (selectedTags.length === 1 && selectedTags[0] === ''))) { - errorIndexes.push(i); - } - } - if (errorIndexes.length !== 0) { - newTransactionViolations.push({ - name: CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED, - type: 'violation', - data: { - errorIndexes, - }, - }); - } else { - let hasInvalidTag = false; - for (let i = 0; i < policyTagKeys.length; i++) { - const selectedTag = selectedTags[i]; - const tags = policyTagList[policyTagKeys[i]].tags; - const isTagInPolicy = Object.values(tags).some((tag) => tag.name === selectedTag && Boolean(tag.enabled)); - if (!isTagInPolicy) { - newTransactionViolations.push({ - name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, - type: 'violation', - data: { - tagName: policyTagKeys[i], - }, - }); - hasInvalidTag = true; - break; - } - } - if (!hasInvalidTag) { - newTransactionViolations = reject(newTransactionViolations, { - name: CONST.VIOLATIONS.TAG_OUT_OF_POLICY, - }); - } - } + newTransactionViolations = getTagViolationsForMultiLevelTags(updatedTransaction, newTransactionViolations, policyRequiresTags, policyTagList); } } From e77b01a5dfd63f473620b33c99837a2eb604e0a7 Mon Sep 17 00:00:00 2001 From: Carlos Alvarez Date: Fri, 8 Mar 2024 10:58:08 -0800 Subject: [PATCH 18/18] Clean up --- src/libs/Violations/ViolationsUtils.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index f17210cafb65..fe2e5af537a7 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -20,9 +20,9 @@ function getTagViolationsForSingleLevelTags( const policyTagListName = policyTagKeys[0]; const policyTags = policyTagList[policyTagListName]?.tags; const hasTagOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.TAG_OUT_OF_POLICY); - let newTransactionViolations = [...transactionViolations]; const hasMissingTagViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.MISSING_TAG); const isTagInPolicy = policyTags ? !!policyTags[updatedTransaction.tag ?? '']?.enabled : false; + let newTransactionViolations = [...transactionViolations]; // Add 'tagOutOfPolicy' violation if tag is not in policy if (!hasTagOutOfPolicyViolation && updatedTransaction.tag && !isTagInPolicy) { @@ -44,7 +44,7 @@ function getTagViolationsForSingleLevelTags( newTransactionViolations.push({name: CONST.VIOLATIONS.MISSING_TAG, type: 'violation'}); } return newTransactionViolations; -}; +} /** * Calculates some tag levels required and missing tag violations for the given transaction @@ -104,9 +104,8 @@ function getTagViolationsForMultiLevelTags( }); } } - return newTransactionViolations; -}; - + return newTransactionViolations; +} const ViolationsUtils = { /** @@ -123,6 +122,7 @@ const ViolationsUtils = { ): OnyxUpdate { let newTransactionViolations = [...transactionViolations]; + // Calculate client-side category violations if (policyRequiresCategories) { const hasCategoryOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === 'categoryOutOfPolicy'); const hasMissingCategoryViolation = transactionViolations.some((violation) => violation.name === 'missingCategory'); @@ -150,13 +150,12 @@ const ViolationsUtils = { } } + // Calculate client-side tag violations if (policyRequiresTags) { - const policyTagKeys = Object.keys(policyTagList); - if (policyTagKeys.length === 1) { - newTransactionViolations = getTagViolationsForSingleLevelTags(updatedTransaction, newTransactionViolations, policyRequiresTags, policyTagList); - } else { - newTransactionViolations = getTagViolationsForMultiLevelTags(updatedTransaction, newTransactionViolations, policyRequiresTags, policyTagList); - } + newTransactionViolations = + Object.keys(policyTagList).length === 1 + ? getTagViolationsForSingleLevelTags(updatedTransaction, newTransactionViolations, policyRequiresTags, policyTagList) + : getTagViolationsForMultiLevelTags(updatedTransaction, newTransactionViolations, policyRequiresTags, policyTagList); } return {