-
Notifications
You must be signed in to change notification settings - Fork 3k
/
Copy pathuseViolations.ts
135 lines (121 loc) · 5.61 KB
/
useViolations.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import {useCallback, useMemo} from 'react';
import CONST from '@src/CONST';
import type {TransactionViolation, ViolationName} from '@src/types/onyx';
/**
* Names of Fields where violations can occur.
*/
type ViolationField = 'amount' | 'billable' | 'category' | 'comment' | 'date' | 'merchant' | 'receipt' | 'tag' | 'tax' | 'customUnitRateID' | 'none';
/**
* 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: 'customUnitRateID',
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',
taxRateChanged: 'tax',
taxAmountChanged: 'tax',
taxOutOfPolicy: 'tax',
taxRequired: 'tax',
hold: 'none',
};
type ViolationsMap = Map<ViolationField, TransactionViolation[]>;
// We don't want to show these violations on NewDot
const excludedViolationsName = ['taxAmountChanged', 'taxRateChanged'];
/**
* @param violations – List of transaction violations
* @param shouldShowOnlyViolations – Whether we should only show violations of type 'violation'
*/
function useViolations(violations: TransactionViolation[], shouldShowOnlyViolations: boolean) {
const violationsByField = useMemo((): ViolationsMap => {
const filteredViolations = violations.filter((violation) => {
if (excludedViolationsName.includes(violation.name)) {
return false;
}
if (shouldShowOnlyViolations) {
return violation.type === CONST.VIOLATION_TYPES.VIOLATION;
}
return true;
});
const violationGroups = new Map<ViolationField, TransactionViolation[]>();
for (const violation of filteredViolations) {
const field = violationFields[violation.name];
const existingViolations = violationGroups.get(field) ?? [];
violationGroups.set(field, [...existingViolations, violation]);
}
return violationGroups ?? new Map();
}, [violations, shouldShowOnlyViolations]);
const getViolationsForField = useCallback(
(field: ViolationField, data?: TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string) => {
const currentViolations = violationsByField.get(field) ?? [];
const firstViolation = currentViolations.at(0);
// 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 (firstViolation?.name === CONST.VIOLATIONS.SOME_TAG_LEVELS_REQUIRED && data?.tagListIndex !== undefined && Array.isArray(firstViolation?.data?.errorIndexes)) {
return currentViolations
.filter((violation) => violation.data?.errorIndexes?.includes(data?.tagListIndex ?? -1))
.map((violation) => ({
...violation,
data: {
...violation.data,
tagName: data?.tagListName,
},
}));
}
// missingTag has special logic for policies with dependent tags, because only one violation is returned for all tags
// when no tags are present, so the tag name isn't set in the violation data. That's why we add it here
if (policyHasDependentTags && firstViolation?.name === CONST.VIOLATIONS.MISSING_TAG && data?.tagListName) {
return [
{
...firstViolation,
data: {
...firstViolation.data,
tagName: data?.tagListName,
},
},
];
}
// 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 (firstViolation?.name === CONST.VIOLATIONS.TAG_OUT_OF_POLICY && data?.tagListName !== undefined && firstViolation?.data?.tagName) {
return currentViolations.filter((violation) => violation.data?.tagName === data?.tagListName);
}
// allTagLevelsRequired has special logic because it is returned when one but not all the tags are set,
// so we need to return the violation for the tag fields without a tag set
if (firstViolation?.name === CONST.VIOLATIONS.ALL_TAG_LEVELS_REQUIRED && tagValue) {
return currentViolations.filter((violation) => violation.data?.tagName === data?.tagListName);
}
return currentViolations;
},
[violationsByField],
);
return {
getViolationsForField,
};
}
export default useViolations;
export type {ViolationField};