diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index a5a969adb833..414570cafe5c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -261,6 +261,7 @@ const ONYXKEYS = { REPORT_USER_IS_LEAVING_ROOM: 'reportUserIsLeavingRoom_', SECURITY_GROUP: 'securityGroup_', TRANSACTION: 'transactions_', + TRANSACTION_VIOLATIONS: 'transactionViolations_', SPLIT_TRANSACTION_DRAFT: 'splitTransactionDraft_', PRIVATE_NOTES_DRAFT: 'privateNotesDraft_', NEXT_STEP: 'reportNextStep_', diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts new file mode 100644 index 000000000000..28f5aedf10b9 --- /dev/null +++ b/src/libs/Violations/ViolationsUtils.ts @@ -0,0 +1,69 @@ +import reject from 'lodash/reject'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {PolicyCategories, PolicyTags, Transaction, TransactionViolation} from '@src/types/onyx'; +import possibleViolationsByField, {ViolationField} from './possibleViolationsByField'; + +const ViolationsUtils = { + getViolationForField(transactionViolations: TransactionViolation[], field: ViolationField, translate: (key: string) => string): string[] { + return transactionViolations.filter((violation) => possibleViolationsByField[field]?.includes(violation.name)).map((violation) => translate(violation.name)); + }, + + getViolationsOnyxData( + /** The transaction to check for policy violations. */ + transaction: Transaction, + /** An array of existing transaction violations. */ + transactionViolations: TransactionViolation[], + /** Indicates if the policy requires tags. */ + policyRequiresTags: boolean, + /** Collection of policy tags and their enabled states. */ + policyTags: PolicyTags, + /** Indicates if the policy requires categories. */ + policyRequiresCategories: boolean, + /** Collection of policy categories and their enabled states. */ + policyCategories: PolicyCategories, + ): { + onyxMethod: string; + key: string; + value: TransactionViolation[]; + } { + let newTransactionViolations = [...transactionViolations]; + + if (policyRequiresCategories) { + const categoryViolationExists = transactionViolations.some((violation) => violation.name === 'categoryOutOfPolicy'); + const categoryIsInPolicy = policyCategories[transaction.category]?.enabled; + + // Add 'categoryOutOfPolicy' violation if category is not in policy + if (!categoryViolationExists && transaction.category && !categoryIsInPolicy) { + newTransactionViolations.push({name: 'categoryOutOfPolicy', type: 'violation', userMessage: ''}); + } + + // Remove 'missingCategory' violation if category is valid according to policy + if (categoryIsInPolicy) { + newTransactionViolations = reject(newTransactionViolations, {name: 'missingCategory'}); + } + } + + if (policyRequiresTags) { + // Add 'tagOutOfPolicy' violation if tag is not in policy + const tagViolationExists = transactionViolations.some((violation) => violation.name === 'tagOutOfPolicy'); + const tagInPolicy = policyTags[transaction.tag]?.enabled; + if (!tagViolationExists && transaction.tag && !tagInPolicy) { + newTransactionViolations.push({name: 'tagOutOfPolicy', type: 'violation', userMessage: ''}); + } + + // Remove 'missingTag' violation if tag is valid according to policy + if (tagInPolicy) { + newTransactionViolations = reject(newTransactionViolations, {name: 'missingTag'}); + } + } + + return { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, + value: newTransactionViolations, + }; + }, +}; + +export default ViolationsUtils; diff --git a/src/libs/Violations/index.ts b/src/libs/Violations/index.ts new file mode 100644 index 000000000000..3ee08a4000d0 --- /dev/null +++ b/src/libs/Violations/index.ts @@ -0,0 +1,3 @@ +import * as ViolationsUtils from './ViolationsUtils'; + +export default ViolationsUtils; diff --git a/src/libs/Violations/possibleViolationsByField.ts b/src/libs/Violations/possibleViolationsByField.ts new file mode 100644 index 000000000000..4dfc1c22b054 --- /dev/null +++ b/src/libs/Violations/possibleViolationsByField.ts @@ -0,0 +1,41 @@ +import invertBy from 'lodash/invertBy'; +import {ViolationName} from '@src/types/onyx'; + +/** + * Map from Violation Names to the field where that violation can occur + */ +const violationFields: Record = { + perDayLimit: 'amount', + maxAge: 'date', + overLimit: 'amount', + overLimitAttendee: 'amount', + overCategoryLimit: 'amount', + receiptRequired: 'receipt', + missingCategory: 'category', + categoryOutOfPolicy: 'category', + missingTag: 'tag', + tagOutOfPolicy: 'tag', + missingComment: 'comment', + taxRequired: 'tax', + taxOutOfPolicy: 'tax', + billableExpense: 'billable', +}; + +/** + * Names of Fields where violations can occur + */ +type ViolationField = 'merchant' | 'amount' | 'category' | 'date' | 'tag' | 'comment' | 'billable' | 'receipt' | 'tax'; + +/** + * Map from field name to array of violation types that can occur on that field. + * @example + * { + * // ... + * category: ['missingCategory', 'categoryOutOfPolicy'] + * // ... + * } + */ +const possibleViolationsByField = invertBy(violationFields, (value) => value) as Record; + +export default possibleViolationsByField; +export type {ViolationField}; diff --git a/src/types/onyx/PolicyCategory.ts b/src/types/onyx/PolicyCategory.ts index adaf16e1acec..b6dfb7bbab9a 100644 --- a/src/types/onyx/PolicyCategory.ts +++ b/src/types/onyx/PolicyCategory.ts @@ -19,4 +19,6 @@ type PolicyCategory = { origin: string; }; +type PolicyCategories = Record; export default PolicyCategory; +export type {PolicyCategories}; diff --git a/src/types/onyx/TransactionViolation.ts b/src/types/onyx/TransactionViolation.ts new file mode 100644 index 000000000000..eb0e67cc1e14 --- /dev/null +++ b/src/types/onyx/TransactionViolation.ts @@ -0,0 +1,34 @@ +/** + * @module TransactionViolation + * @description Transaction Violation + */ + +/** + * Names of the various Transaction Violation types + */ +type ViolationName = + | 'perDayLimit' + | 'maxAge' + | 'overLimit' + | 'overLimitAttendee' + | 'overCategoryLimit' + | 'receiptRequired' + | 'missingCategory' + | 'categoryOutOfPolicy' + | 'missingTag' + | 'tagOutOfPolicy' + | 'missingComment' + | 'taxRequired' + | 'taxOutOfPolicy' + | 'billableExpense'; + +type ViolationType = string; + +type TransactionViolation = { + type: ViolationType; + name: ViolationName; + userMessage: string; + data?: Record; +}; + +export type {TransactionViolation, ViolationName, ViolationType}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 87cf24d6dec7..adeba62b08c6 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -21,7 +21,7 @@ import PersonalBankAccount from './PersonalBankAccount'; import PersonalDetails from './PersonalDetails'; import PlaidData from './PlaidData'; import Policy from './Policy'; -import PolicyCategory from './PolicyCategory'; +import PolicyCategory, {PolicyCategories} from './PolicyCategory'; import PolicyMember, {PolicyMembers} from './PolicyMember'; import PolicyTag, {PolicyTags} from './PolicyTag'; import PrivatePersonalDetails from './PrivatePersonalDetails'; @@ -42,6 +42,7 @@ import SecurityGroup from './SecurityGroup'; import Session from './Session'; import Task from './Task'; import Transaction from './Transaction'; +import {TransactionViolation, ViolationName, ViolationType} from './TransactionViolation'; import User from './User'; import UserWallet from './UserWallet'; import WalletAdditionalDetails from './WalletAdditionalDetails'; @@ -78,6 +79,7 @@ export type { PlaidData, Policy, PolicyCategory, + PolicyCategories, PolicyMember, PolicyMembers, PolicyTag, @@ -101,8 +103,11 @@ export type { Session, Task, Transaction, + TransactionViolation, User, UserWallet, + ViolationName, + ViolationType, WalletAdditionalDetails, WalletOnfido, WalletStatement,