Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
f251978
feat(Violations): Add PolicyCategories type to match PolicyTags
trevor-coleman Nov 17, 2023
51c2e7e
feat(Violations): Create stub for ViolationUtils
trevor-coleman Nov 17, 2023
f001066
feat(Violations): Add TRANSACTION_VIOLATION to ONYXKEYS.COLLECTIONS
trevor-coleman Nov 17, 2023
2705280
feat(Violations): Add TransactionViolation and related types to src/t…
trevor-coleman Nov 17, 2023
e2aaa6a
feat(Violations): create possibleViolationsByField lookup
trevor-coleman Nov 17, 2023
7f39f5a
feat(Violations): implement getViolationForField and getViolationsOny…
trevor-coleman Nov 17, 2023
ccb4206
prettier
trevor-coleman Nov 20, 2023
6a67fb8
Merge pull request #73 from infinitered/trevorcoleman/violations/viol…
trevor-coleman Nov 21, 2023
0a04347
feat(Violations): remove unnecessary callback
trevor-coleman Nov 21, 2023
15f43ba
Merge remote-tracking branch 'origin/main' into trevorcoleman/violati…
trevor-coleman Nov 22, 2023
575ea9b
update package-lock.json
trevor-coleman Nov 22, 2023
17ef290
Merge remote-tracking branch 'upstream/main' into trevorcoleman/viola…
trevor-coleman Nov 22, 2023
cf65dbe
Merge remote-tracking branch 'upstream/main' into violation-utils
trevor-coleman Nov 22, 2023
0353afa
restore package-lock.json
trevor-coleman Nov 22, 2023
8c16f79
Merge remote-tracking branch 'upstream/main' into violation-utils
trevor-coleman Nov 22, 2023
1e2e7c8
Update src/types/onyx/TransactionViolation.ts
trevor-coleman Nov 28, 2023
0c8d067
Update src/libs/Violations/ViolationsUtils.ts
trevor-coleman Nov 28, 2023
434fd9f
Update src/types/onyx/TransactionViolation.ts
trevor-coleman Nov 28, 2023
d251b9a
Update src/types/onyx/TransactionViolation.ts
trevor-coleman Nov 28, 2023
19b11b1
Merge branch 'Expensify:main' into violation-utils
trevor-coleman Nov 28, 2023
7f83963
feat(Violations): remove ViolationType, sort violation names
trevor-coleman Nov 28, 2023
e4a56b9
feat(Violations): add dummy translations for complete list
trevor-coleman Nov 28, 2023
12a2aa3
feat(Violations): remove barrel file
trevor-coleman Nov 28, 2023
12682bb
feat(Violations): extract getViolationsForField into useViolations hook
trevor-coleman Nov 28, 2023
d7ca133
Merge branch 'violation-utils' into trevorcoleman/violations/violatio…
trevor-coleman Nov 28, 2023
58ea8d8
feat(Violations): export violationFields for testing
trevor-coleman Nov 28, 2023
8f951e0
feat(Violations): add tests for useViolations
trevor-coleman Nov 28, 2023
04c23f6
feat(Violations): remove comments on arguments and replace with JSDoc
trevor-coleman Nov 28, 2023
d1c8792
Merge remote-tracking branch 'origin/trevorcoleman/violations/violati…
trevor-coleman Nov 28, 2023
ddbe029
feat(Violations): lint
trevor-coleman Nov 28, 2023
7054d05
package-lock
trevor-coleman Nov 28, 2023
532c96a
prettier
trevor-coleman Nov 28, 2023
6568133
feat(Violations): remove possibleViolationsByField
trevor-coleman Nov 28, 2023
6aef0ba
Merge pull request #77 from infinitered/trevorcoleman/violations/viol…
trevor-coleman Nov 29, 2023
dfb444b
feat(Violations): update violationFields
trevor-coleman Nov 29, 2023
ae8acc5
feat(Violations): update translations for violations
trevor-coleman Nov 29, 2023
a796c2c
feat(Violations): update spanish translations for violations
trevor-coleman Nov 29, 2023
f2792e5
feat(Violations): create ViolationUtilsTest with stubs.
trevor-coleman Nov 29, 2023
3710f82
feat(Violations): fix missing tags and categories logic
trevor-coleman Nov 29, 2023
5158f20
feat(Violations): add tests for ViolationUtils
trevor-coleman Nov 29, 2023
eaa9c85
feat(Violations): remove test for case sensitivity
trevor-coleman Nov 29, 2023
4fcc994
feat(Violations): add tests for ViolationUtils
trevor-coleman Nov 29, 2023
a66d253
feat(Violations): return dummy translations
trevor-coleman Nov 29, 2023
f504508
feat(Violations): remove unused imports
trevor-coleman Nov 29, 2023
ae1731b
feat(Violations): memoize return values for hasViolations, and return…
trevor-coleman Nov 29, 2023
de45660
feat(Violations): update tests for new return values.
trevor-coleman Nov 29, 2023
e239e41
Merge pull request #78 from infinitered/trevor-coleman/violations/vio…
trevor-coleman Nov 29, 2023
2b27509
feat(Violations): remove index
trevor-coleman Nov 29, 2023
47afbe9
feat(Violations): add quotes
trevor-coleman Nov 30, 2023
bc3b872
feat(Violations): Remove unnecessary casts
trevor-coleman Nov 30, 2023
9fd9bf9
feat(Violations): capitalize
trevor-coleman Nov 30, 2023
e72bacc
feat(Violations): Add condition
trevor-coleman Nov 30, 2023
2de9615
feat(Violations): Remove cast
trevor-coleman Nov 30, 2023
478f934
feat(Violations): add condition
trevor-coleman Nov 30, 2023
ef5ebb3
feat(Violations): remove unused types
trevor-coleman Nov 30, 2023
68c5485
feat(Violations): harmonize tag and category handling
trevor-coleman Nov 30, 2023
75f003a
feat(Violations): reorder types
trevor-coleman Nov 30, 2023
016b438
feat(Violations): Apply suggestions from code review
trevor-coleman Nov 30, 2023
9c356f2
feat(Violations): remove hasViolationsMap
trevor-coleman Nov 30, 2023
9b0c88a
feat(Violations): refactor useViolations tests
trevor-coleman Nov 30, 2023
e0d8d97
feat(Violations): refactor ViolationUtils tests
trevor-coleman Dec 1, 2023
96544c9
Merge remote-tracking branch 'origin/violation-utils' into violation-…
trevor-coleman Dec 1, 2023
06b7012
Merge branch 'main' into violation-utils
trevor-coleman Dec 1, 2023
ebb5751
feat(Violations): revert package.json
trevor-coleman Dec 1, 2023
74b80df
prettier
trevor-coleman Dec 1, 2023
d3be1f6
feat(Violations): remove execute
trevor-coleman Dec 1, 2023
4ab9931
feat(Violations): refactor tests
trevor-coleman Dec 1, 2023
3737752
feat(Violations): refactor tests
trevor-coleman Dec 1, 2023
1876a0b
feat(Violations): refactor tests
trevor-coleman Dec 1, 2023
72667ec
feat(Violations): refactor tests to have simpler base case
trevor-coleman Dec 1, 2023
27cfac1
Update tests/unit/ViolationUtilsTest.js
trevor-coleman Dec 4, 2023
e5ae614
feat(Violations): Suggested changes from PR Review
trevor-coleman Dec 4, 2023
8498e80
feat(Violations): remove useViolationsTest
trevor-coleman Dec 4, 2023
e6e7612
feat(Violations): fixes from PR Comments
trevor-coleman Dec 4, 2023
0aa36b8
Merge remote-tracking branch 'origin/violation-utils' into violation-…
trevor-coleman Dec 4, 2023
391822c
prettier
trevor-coleman Dec 4, 2023
dd48fe1
Merge remote-tracking branch 'upstream/main' into violation-utils
trevor-coleman Dec 4, 2023
d2cfd33
feat(Violations): remove useViolationsTest
trevor-coleman Dec 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: {
Comment thread
trevor-coleman marked this conversation as resolved.
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