Skip to content

Commit

Permalink
Merge pull request #77 from infinitered/trevorcoleman/violations/viol…
Browse files Browse the repository at this point in the history
…ation-utils

Trevorcoleman/violations/violation utils
  • Loading branch information
trevor-coleman authored Nov 29, 2023
2 parents 19b11b1 + 6568133 commit 6aef0ba
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 4,124 deletions.
4,088 changes: 49 additions & 4,039 deletions package-lock.json

Large diffs are not rendered by default.

35 changes: 35 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1974,4 +1974,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 @@ -2459,4 +2459,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;
36 changes: 22 additions & 14 deletions src/libs/Violations/ViolationsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@ 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));
},

/**
* Computes an updated array of transaction violations for the given transaction
* Checks a transaction for policy violations and returns an object with Onyx method, key and updated transaction violations.
*/
getViolationsOnyxData(
transaction: Transaction,
Expand All @@ -27,30 +22,43 @@ const ViolationsUtils = {
let newTransactionViolations = [...transactionViolations];

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

const isCategoryInPolicy = Boolean(policyCategories[transaction.category]?.enabled);

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

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

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

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

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

// 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) {
if (!hasTagViolation && transaction.tag && !isTagInPolicy) {
newTransactionViolations.push({name: 'tagOutOfPolicy', type: 'violation', userMessage: ''});
}

// Remove 'missingTag' violation if tag is valid according to policy
if (tagInPolicy) {
if (isTagInPolicy) {
newTransactionViolations = reject(newTransactionViolations, {name: 'missingTag'});
}
}
Expand Down
41 changes: 0 additions & 41 deletions src/libs/Violations/possibleViolationsByField.ts

This file was deleted.

88 changes: 88 additions & 0 deletions src/libs/Violations/useViolations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {useCallback, useMemo} from 'react';
import useLocalize from '@hooks/useLocalize';
import {TransactionViolation, ViolationName} from '@src/types/onyx';

/**
* Map from Violation Names to the field where that violation can occur
*/
const violationFields: Record<ViolationName, ViolationField> = {
allTagLevelsRequired: 'tag',
autoReportedRejectedExpense: 'amount',
billableExpense: 'billable',
cashExpenseWithNoReceipt: 'receipt',
categoryOutOfPolicy: 'category',
conversionSurcharge: 'amount',
customUnitOutOfPolicy: 'amount',
duplicatedTransaction: 'merchant',
fieldRequired: 'category',
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',
};

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

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

function useViolations(violations: TransactionViolation[]) {
const {translate} = useLocalize();

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;
}, [violations]);

const hasViolations = useCallback(
(field: ViolationField) => {
const fieldViolations: TransactionViolation[] = violationsByField.get(field) ?? [];
return Boolean(fieldViolations.length > 0);
},
[violationsByField],
);

const getViolationsForField = useCallback(
(field: ViolationField) => {
const fieldViolations: TransactionViolation[] = violationsByField.get(field) ?? [];
return fieldViolations.map((violation) => translate(`violations.${violation.name}`));
},
[translate, violationsByField],
);

return {
hasViolations,
getViolationsForField,
};
}

export {useViolations, violationFields};
54 changes: 26 additions & 28 deletions src/types/onyx/TransactionViolation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,45 @@
* Names of transaction violations
*/
type ViolationName =
| 'allTagLevelsRequired'
| 'autoReportedRejectedExpense'
| 'billableExpense'
| 'cashExpenseWithNoReceipt'
| 'categoryOutOfPolicy'
| 'conversionSurcharge'
| 'customUnitOutOfPolicy'
| 'duplicatedTransaction'
| 'fieldRequired'
| 'perDayLimit'
| 'rter'
| 'maxAge'
| 'futureDate'
| 'invoiceMarkup'
| 'maxAge'
| 'missingCategory'
| 'missingComment'
| 'missingTag'
| 'modifiedAmount'
| 'modifiedDate'
| 'nonExpensiworksExpense'
| 'overAutoApprovalLimit'
| 'overCategoryLimit'
| 'overLimit'
| 'overLimitAttendee'
| 'overCategoryLimit'
| 'perDayLimit'
| 'receiptNotSmartScanned'
| 'receiptRequired'
| 'missingCategory'
| 'categoryOutOfPolicy'
| 'missingTag'
| 'rter'
| 'smartscanFailed'
| 'someTagLevelsRequired'
| 'allTagLevelsRequired'
| 'tagOutOfPolicy'
| 'missingComment'
| 'taxRequired'
| 'taxAmountChanged'
| 'taxOutOfPolicy'
| 'taxRateChanged'
| 'taxAmountChanged'
| 'modifiedAmount'
| 'receiptNotSmartScanned'
| 'modifiedDate'
| 'cashExpenseWithNoReceipt'
| 'invoiceMarkup'
| 'duplicatedTransaction'
| 'smartscanFailed'
| 'conversionSurcharge'
| 'autoReportedRejectedExpense'
| 'nonExpensiworksExpense'
| 'billableExpense'
| 'customUnitOutOfPolicy'
| 'overAutoApprovalLimit'

type ViolationType = string;
| 'taxRequired';

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

export type {TransactionViolation, ViolationName, ViolationType};
export type {TransactionViolation, ViolationName};
3 changes: 1 addition & 2 deletions src/types/onyx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +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 {TransactionViolation, ViolationName} from './TransactionViolation';
import User from './User';
import UserLocation from './UserLocation';
import UserWallet from './UserWallet';
Expand Down Expand Up @@ -109,7 +109,6 @@ export type {
User,
UserWallet,
ViolationName,
ViolationType,
WalletAdditionalDetails,
WalletOnfido,
WalletStatement,
Expand Down
Loading

0 comments on commit 6aef0ba

Please sign in to comment.