Skip to content

Commit

Permalink
Merge pull request #31656 from infinitered/violation-utils
Browse files Browse the repository at this point in the history
  • Loading branch information
cead22 authored Dec 4, 2023
2 parents 9595ed6 + d2cfd33 commit b5e83fa
Show file tree
Hide file tree
Showing 9 changed files with 475 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ const ONYXKEYS = {
REPORT_USER_IS_LEAVING_ROOM: 'reportUserIsLeavingRoom_',
SECURITY_GROUP: 'securityGroup_',
TRANSACTION: 'transactions_',
TRANSACTION_VIOLATIONS: 'transactionViolations_',

// Holds temporary transactions used during the creation and edit flow
TRANSACTION_DRAFT: 'transactionsDraft_',
Expand Down
72 changes: 72 additions & 0 deletions src/hooks/useViolations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {useCallback, useMemo} from 'react';
import {TransactionViolation, ViolationName} from '@src/types/onyx';

/**
* Names of Fields where violations can occur
*/
type ViolationField = 'amount' | 'billable' | 'category' | 'comment' | 'date' | 'merchant' | 'receipt' | 'tag' | 'tax';

/**
* Map from Violation Names to the field where that violation can occur
*/
const violationFields: Record<ViolationName, ViolationField> = {
allTagLevelsRequired: 'tag',
autoReportedRejectedExpense: 'merchant',
billableExpense: 'billable',
cashExpenseWithNoReceipt: 'receipt',
categoryOutOfPolicy: 'category',
conversionSurcharge: 'amount',
customUnitOutOfPolicy: 'merchant',
duplicatedTransaction: 'merchant',
fieldRequired: 'merchant',
futureDate: 'date',
invoiceMarkup: 'amount',
maxAge: 'date',
missingCategory: 'category',
missingComment: 'comment',
missingTag: 'tag',
modifiedAmount: 'amount',
modifiedDate: 'date',
nonExpensiworksExpense: 'merchant',
overAutoApprovalLimit: 'amount',
overCategoryLimit: 'amount',
overLimit: 'amount',
overLimitAttendee: 'amount',
perDayLimit: 'amount',
receiptNotSmartScanned: 'receipt',
receiptRequired: 'receipt',
rter: 'merchant',
smartscanFailed: 'receipt',
someTagLevelsRequired: 'tag',
tagOutOfPolicy: 'tag',
taxAmountChanged: 'tax',
taxOutOfPolicy: 'tax',
taxRateChanged: 'tax',
taxRequired: 'tax',
};

type ViolationsMap = Map<ViolationField, TransactionViolation[]>;

function useViolations(violations: TransactionViolation[]) {
const violationsByField = useMemo((): ViolationsMap => {
const violationGroups = new Map<ViolationField, TransactionViolation[]>();

for (const violation of violations) {
const field = violationFields[violation.name];
const existingViolations = violationGroups.get(field) ?? [];
violationGroups.set(field, [...existingViolations, violation]);
}

return violationGroups ?? new Map();
}, [violations]);

const hasViolations = useCallback((field: ViolationField) => Boolean(violationsByField.get(field)?.length), [violationsByField]);
const getViolationsForField = useCallback((field: ViolationField) => violationsByField.get(field) ?? [], [violationsByField]);

return {
hasViolations,
getViolationsForField,
};
}

export default useViolations;
35 changes: 35 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1979,4 +1979,39 @@ export default {
},
copyReferralLink: 'Copy referral link',
},
violations: {
allTagLevelsRequired: 'dummy.violations.allTagLevelsRequired',
autoReportedRejectedExpense: 'dummy.violations.autoReportedRejectedExpense',
billableExpense: 'dummy.violations.billableExpense',
cashExpenseWithNoReceipt: 'dummy.violations.cashExpenseWithNoReceipt',
categoryOutOfPolicy: 'dummy.violations.categoryOutOfPolicy',
conversionSurcharge: 'dummy.violations.conversionSurcharge',
customUnitOutOfPolicy: 'dummy.violations.customUnitOutOfPolicy',
duplicatedTransaction: 'dummy.violations.duplicatedTransaction',
fieldRequired: 'dummy.violations.fieldRequired',
futureDate: 'dummy.violations.futureDate',
invoiceMarkup: 'dummy.violations.invoiceMarkup',
maxAge: 'dummy.violations.maxAge',
missingCategory: 'dummy.violations.missingCategory',
missingComment: 'dummy.violations.missingComment',
missingTag: 'dummy.violations.missingTag',
modifiedAmount: 'dummy.violations.modifiedAmount',
modifiedDate: 'dummy.violations.modifiedDate',
nonExpensiworksExpense: 'dummy.violations.nonExpensiworksExpense',
overAutoApprovalLimit: 'dummy.violations.overAutoApprovalLimit',
overCategoryLimit: 'dummy.violations.overCategoryLimit',
overLimit: 'dummy.violations.overLimit',
overLimitAttendee: 'dummy.violations.overLimitAttendee',
perDayLimit: 'dummy.violations.perDayLimit',
receiptNotSmartScanned: 'dummy.violations.receiptNotSmartScanned',
receiptRequired: 'dummy.violations.receiptRequired',
rter: 'dummy.violations.rter',
smartscanFailed: 'dummy.violations.smartscanFailed',
someTagLevelsRequired: 'dummy.violations.someTagLevelsRequired',
tagOutOfPolicy: 'dummy.violations.tagOutOfPolicy',
taxAmountChanged: 'dummy.violations.taxAmountChanged',
taxOutOfPolicy: 'dummy.violations.taxOutOfPolicy',
taxRateChanged: 'dummy.violations.taxRateChanged',
taxRequired: 'dummy.violations.taxRequired',
},
} satisfies TranslationBase;
35 changes: 35 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2464,4 +2464,39 @@ export default {
},
copyReferralLink: 'Copiar enlace de invitación',
},
violations: {
allTagLevelsRequired: 'dummy.violations.allTagLevelsRequired',
autoReportedRejectedExpense: 'dummy.violations.autoReportedRejectedExpense',
billableExpense: 'dummy.violations.billableExpense',
cashExpenseWithNoReceipt: 'dummy.violations.cashExpenseWithNoReceipt',
categoryOutOfPolicy: 'dummy.violations.categoryOutOfPolicy',
conversionSurcharge: 'dummy.violations.conversionSurcharge',
customUnitOutOfPolicy: 'dummy.violations.customUnitOutOfPolicy',
duplicatedTransaction: 'dummy.violations.duplicatedTransaction',
fieldRequired: 'dummy.violations.fieldRequired',
futureDate: 'dummy.violations.futureDate',
invoiceMarkup: 'dummy.violations.invoiceMarkup',
maxAge: 'dummy.violations.maxAge',
missingCategory: 'dummy.violations.missingCategory',
missingComment: 'dummy.violations.missingComment',
missingTag: 'dummy.violations.missingTag',
modifiedAmount: 'dummy.violations.modifiedAmount',
modifiedDate: 'dummy.violations.modifiedDate',
nonExpensiworksExpense: 'dummy.violations.nonExpensiworksExpense',
overAutoApprovalLimit: 'dummy.violations.overAutoApprovalLimit',
overCategoryLimit: 'dummy.violations.overCategoryLimit',
overLimit: 'dummy.violations.overLimit',
overLimitAttendee: 'dummy.violations.overLimitAttendee',
perDayLimit: 'dummy.violations.perDayLimit',
receiptNotSmartScanned: 'dummy.violations.receiptNotSmartScanned',
receiptRequired: 'dummy.violations.receiptRequired',
rter: 'dummy.violations.rter',
smartscanFailed: 'dummy.violations.smartscanFailed',
someTagLevelsRequired: 'dummy.violations.someTagLevelsRequired',
tagOutOfPolicy: 'dummy.violations.tagOutOfPolicy',
taxAmountChanged: 'dummy.violations.taxAmountChanged',
taxOutOfPolicy: 'dummy.violations.taxOutOfPolicy',
taxRateChanged: 'dummy.violations.taxRateChanged',
taxRequired: 'dummy.violations.taxRequired',
},
} satisfies EnglishTranslation;
85 changes: 85 additions & 0 deletions src/libs/ViolationsUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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';

const ViolationsUtils = {
/**
* Checks a transaction for policy violations and returns an object with Onyx method, key and updated transaction
* violations.
*/
getViolationsOnyxData(
transaction: Transaction,
transactionViolations: TransactionViolation[],
policyRequiresTags: boolean,
policyTags: PolicyTags,
policyRequiresCategories: boolean,
policyCategories: PolicyCategories,
): {
onyxMethod: string;
key: string;
value: TransactionViolation[];
} {
let newTransactionViolations = [...transactionViolations];

if (policyRequiresCategories) {
const hasCategoryOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === 'categoryOutOfPolicy');
const hasMissingCategoryViolation = transactionViolations.some((violation) => violation.name === 'missingCategory');
const isCategoryInPolicy = Boolean(policyCategories[transaction.category]?.enabled);

// Add 'categoryOutOfPolicy' violation if category is not in policy
if (!hasCategoryOutOfPolicyViolation && transaction.category && !isCategoryInPolicy) {
newTransactionViolations.push({name: 'categoryOutOfPolicy', type: 'violation', userMessage: ''});
}

// Remove 'categoryOutOfPolicy' violation if category is in policy
if (hasCategoryOutOfPolicyViolation && transaction.category && isCategoryInPolicy) {
newTransactionViolations = reject(newTransactionViolations, {name: 'categoryOutOfPolicy'});
}

// Remove 'missingCategory' violation if category is valid according to policy
if (hasMissingCategoryViolation && isCategoryInPolicy) {
newTransactionViolations = reject(newTransactionViolations, {name: 'missingCategory'});
}

// Add 'missingCategory' violation if category is required and not set
if (!hasMissingCategoryViolation && policyRequiresCategories && !transaction.category) {
newTransactionViolations.push({name: 'missingCategory', type: 'violation', userMessage: ''});
}
}

if (policyRequiresTags) {
const hasTagOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === 'tagOutOfPolicy');
const hasMissingTagViolation = transactionViolations.some((violation) => violation.name === 'missingTag');
const isTagInPolicy = Boolean(policyTags[transaction.tag]?.enabled);

// Add 'tagOutOfPolicy' violation if tag is not in policy
if (!hasTagOutOfPolicyViolation && transaction.tag && !isTagInPolicy) {
newTransactionViolations.push({name: 'tagOutOfPolicy', type: 'violation', userMessage: ''});
}

// Remove 'tagOutOfPolicy' violation if tag is in policy
if (hasTagOutOfPolicyViolation && transaction.tag && isTagInPolicy) {
newTransactionViolations = reject(newTransactionViolations, {name: 'tagOutOfPolicy'});
}

// Remove 'missingTag' violation if tag is valid according to policy
if (hasMissingTagViolation && isTagInPolicy) {
newTransactionViolations = reject(newTransactionViolations, {name: 'missingTag'});
}

// Add 'missingTag violation' if tag is required and not set
if (!hasMissingTagViolation && !transaction.tag && policyRequiresTags) {
newTransactionViolations.push({name: 'missingTag', type: 'violation', userMessage: ''});
}
}

return {
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`,
value: newTransactionViolations,
};
},
};

export default ViolationsUtils;
2 changes: 2 additions & 0 deletions src/types/onyx/PolicyCategory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ type PolicyCategory = {
origin: string;
};

type PolicyCategories = Record<string, PolicyCategory>;
export default PolicyCategory;
export type {PolicyCategories};
46 changes: 46 additions & 0 deletions src/types/onyx/TransactionViolation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Names of transaction violations
*/
type ViolationName =
| 'allTagLevelsRequired'
| 'autoReportedRejectedExpense'
| 'billableExpense'
| 'cashExpenseWithNoReceipt'
| 'categoryOutOfPolicy'
| 'conversionSurcharge'
| 'customUnitOutOfPolicy'
| 'duplicatedTransaction'
| 'fieldRequired'
| 'futureDate'
| 'invoiceMarkup'
| 'maxAge'
| 'missingCategory'
| 'missingComment'
| 'missingTag'
| 'modifiedAmount'
| 'modifiedDate'
| 'nonExpensiworksExpense'
| 'overAutoApprovalLimit'
| 'overCategoryLimit'
| 'overLimit'
| 'overLimitAttendee'
| 'perDayLimit'
| 'receiptNotSmartScanned'
| 'receiptRequired'
| 'rter'
| 'smartscanFailed'
| 'someTagLevelsRequired'
| 'tagOutOfPolicy'
| 'taxAmountChanged'
| 'taxOutOfPolicy'
| 'taxRateChanged'
| 'taxRequired';

type TransactionViolation = {
type: string;
name: ViolationName;
userMessage: string;
data?: Record<string, string>;
};

export type {TransactionViolation, ViolationName};
6 changes: 5 additions & 1 deletion src/types/onyx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -42,6 +42,7 @@ import SecurityGroup from './SecurityGroup';
import Session from './Session';
import Task from './Task';
import Transaction from './Transaction';
import {TransactionViolation, ViolationName} from './TransactionViolation';
import User from './User';
import UserLocation from './UserLocation';
import UserWallet from './UserWallet';
Expand Down Expand Up @@ -80,6 +81,7 @@ export type {
PlaidData,
Policy,
PolicyCategory,
PolicyCategories,
PolicyMember,
PolicyMembers,
PolicyTag,
Expand All @@ -103,8 +105,10 @@ export type {
Session,
Task,
Transaction,
TransactionViolation,
User,
UserWallet,
ViolationName,
WalletAdditionalDetails,
WalletOnfido,
WalletStatement,
Expand Down
Loading

0 comments on commit b5e83fa

Please sign in to comment.