Skip to content

Commit f6efb52

Browse files
authored
Merge pull request Expensify#62234 from Tony-MK/fix/61306
Merging optimistic transactions violations for policy categories
2 parents 461fb5a + 79a889f commit f6efb52

7 files changed

Lines changed: 143 additions & 17 deletions

File tree

src/libs/ReportUtils.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ import type {
3535
PersonalDetails,
3636
PersonalDetailsList,
3737
Policy,
38+
PolicyCategories,
39+
PolicyCategory,
3840
PolicyReportField,
41+
PolicyTagLists,
3942
Report,
4043
ReportAction,
4144
ReportAttributesDerivedValue,
@@ -47,6 +50,7 @@ import type {
4750
Task,
4851
Transaction,
4952
TransactionViolation,
53+
TransactionViolations,
5054
UserWallet,
5155
} from '@src/types/onyx';
5256
import type {Attendee, Participant} from '@src/types/onyx/IOU';
@@ -60,6 +64,7 @@ import type {AllConnectionName, ConnectionName} from '@src/types/onyx/Policy';
6064
import type {InvoiceReceiverType, NotificationPreference, Participants, Participant as ReportParticipant} from '@src/types/onyx/Report';
6165
import type {Message, OldDotReportAction, ReportActions} from '@src/types/onyx/ReportAction';
6266
import type {PendingChatMember} from '@src/types/onyx/ReportMetadata';
67+
import type {OnyxData} from '@src/types/onyx/Request';
6368
import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults';
6469
import type {Comment, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction';
6570
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -116,6 +121,7 @@ import {
116121
getPolicyRole,
117122
getRuleApprovers,
118123
getSubmitToAccountID,
124+
hasDependentTags as hasDependentTagsPolicyUtils,
119125
isExpensifyTeam,
120126
isInstantSubmitEnabled,
121127
isPaidGroupPolicy as isPaidGroupPolicyPolicyUtils,
@@ -248,6 +254,7 @@ import {
248254
import {addTrailingForwardSlash} from './Url';
249255
import type {AvatarSource} from './UserUtils';
250256
import {generateAccountID, getDefaultAvatarURL} from './UserUtils';
257+
import ViolationsUtils from './Violations/ViolationsUtils';
251258

252259
// Dynamic Import to avoid circular dependency
253260
const UnreadIndicatorUpdaterHelper = () => import('./UnreadIndicatorUpdater');
@@ -1794,6 +1801,65 @@ function isAwaitingFirstLevelApproval(report: OnyxEntry<Report>): boolean {
17941801
return isProcessingReport(report) && submitsToAccountID === report.managerID;
17951802
}
17961803

1804+
/**
1805+
* Pushes optimistic transaction violations to OnyxData for the given policy and categories onyx update.
1806+
*
1807+
* @param policyUpdate Changed policy properties, if none pass empty object
1808+
* @param policyCategoriesUpdate Changed categories properties, if none pass empty object
1809+
*/
1810+
function pushTransactionViolationsOnyxData(
1811+
onyxData: OnyxData,
1812+
policyID: string,
1813+
policyTagLists: PolicyTagLists,
1814+
policyCategories: PolicyCategories,
1815+
allTransactionViolations: OnyxCollection<TransactionViolations>,
1816+
policyUpdate: Partial<Policy> = {},
1817+
policyCategoriesUpdate: Record<string, Partial<PolicyCategory>> = {},
1818+
): OnyxData {
1819+
if (isEmptyObject(policyUpdate) && isEmptyObject(policyCategoriesUpdate)) {
1820+
return onyxData;
1821+
}
1822+
const optimisticPolicyCategories = Object.keys(policyCategories).reduce<Record<string, PolicyCategory>>((acc, categoryName) => {
1823+
acc[categoryName] = {...policyCategories[categoryName], ...(policyCategoriesUpdate?.[categoryName] ?? {})};
1824+
return acc;
1825+
}, {}) as PolicyCategories;
1826+
1827+
const optimisticPolicy = {...allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`], ...policyUpdate} as Policy;
1828+
const hasDependentTags = hasDependentTagsPolicyUtils(optimisticPolicy, policyTagLists);
1829+
1830+
getAllPolicyReports(policyID).forEach((report) => {
1831+
if (!report?.reportID) {
1832+
return;
1833+
}
1834+
1835+
const isReportAnInvoice = isInvoiceReport(report);
1836+
1837+
getReportTransactions(report.reportID).forEach((transaction: Transaction) => {
1838+
const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`] ?? [];
1839+
1840+
const optimisticTransactionViolations = ViolationsUtils.getViolationsOnyxData(
1841+
transaction,
1842+
transactionViolations,
1843+
optimisticPolicy,
1844+
policyTagLists,
1845+
optimisticPolicyCategories,
1846+
hasDependentTags,
1847+
isReportAnInvoice,
1848+
);
1849+
1850+
if (optimisticTransactionViolations) {
1851+
onyxData?.optimisticData?.push(optimisticTransactionViolations);
1852+
onyxData?.failureData?.push({
1853+
onyxMethod: Onyx.METHOD.SET,
1854+
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`,
1855+
value: transactionViolations,
1856+
});
1857+
}
1858+
});
1859+
});
1860+
return onyxData;
1861+
}
1862+
17971863
/**
17981864
* Check if the report is a single chat report that isn't a thread
17991865
* and personal detail of participant is optimistic data
@@ -11363,6 +11429,7 @@ export {
1136311429
isAllowedToSubmitDraftExpenseReport,
1136411430
findReportIDForAction,
1136511431
isWorkspaceEligibleForReportChange,
11432+
pushTransactionViolationsOnyxData,
1136611433
navigateOnDeleteExpense,
1136711434
hasReportBeenReopened,
1136811435
getMoneyReportPreviewName,

src/libs/actions/Policy/Category.ts

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,14 @@ import getIsNarrowLayout from '@libs/getIsNarrowLayout';
2727
import {translateLocal} from '@libs/Localize';
2828
import Log from '@libs/Log';
2929
import enhanceParameters from '@libs/Network/enhanceParameters';
30+
import {hasEnabledOptions} from '@libs/OptionsListUtils';
3031
import {getPolicy, goBackWhenEnableFeature} from '@libs/PolicyUtils';
31-
import {getAllPolicyReports} from '@libs/ReportUtils';
32+
import {getAllPolicyReports, pushTransactionViolationsOnyxData} from '@libs/ReportUtils';
3233
import {resolveEnableFeatureConflicts} from '@userActions/RequestConflictUtils';
3334
import {getFinishOnboardingTaskOnyxData} from '@userActions/Task';
3435
import CONST from '@src/CONST';
3536
import ONYXKEYS from '@src/ONYXKEYS';
36-
import type {Policy, PolicyCategories, PolicyCategory, RecentlyUsedCategories, Report} from '@src/types/onyx';
37+
import type {Policy, PolicyCategories, PolicyCategory, PolicyTagLists, RecentlyUsedCategories, Report, TransactionViolations} from '@src/types/onyx';
3738
import type {ApprovalRule, ExpenseRule, MccGroup} from '@src/types/onyx/Policy';
3839
import type {PolicyCategoryExpenseLimitType} from '@src/types/onyx/PolicyCategory';
3940
import type {OnyxData} from '@src/types/onyx/Request';
@@ -301,7 +302,12 @@ function buildOptimisticPolicyRecentlyUsedCategories(policyID?: string, category
301302
return lodashUnion([category], policyRecentlyUsedCategories);
302303
}
303304

304-
function setWorkspaceCategoryEnabled(policyID: string, categoriesToUpdate: Record<string, {name: string; enabled: boolean}>) {
305+
function setWorkspaceCategoryEnabled(
306+
policyID: string,
307+
categoriesToUpdate: Record<string, {name: string; enabled: boolean}>,
308+
policyTagLists: PolicyTagLists = {},
309+
allTransactionViolations: OnyxCollection<TransactionViolations> = {},
310+
) {
305311
const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {};
306312
const optimisticPolicyCategoriesData = {
307313
...Object.keys(categoriesToUpdate).reduce<PolicyCategories>((acc, key) => {
@@ -367,6 +373,7 @@ function setWorkspaceCategoryEnabled(policyID: string, categoriesToUpdate: Recor
367373
],
368374
};
369375

376+
pushTransactionViolationsOnyxData(onyxData, policyID, policyTagLists, policyCategories, allTransactionViolations, {}, optimisticPolicyCategoriesData);
370377
appendSetupCategoriesOnboardingData(onyxData);
371378

372379
const parameters = {
@@ -861,7 +868,12 @@ function setPolicyCategoryGLCode(policyID: string, categoryName: string, glCode:
861868
API.write(WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_GL_CODE, parameters, onyxData);
862869
}
863870

864-
function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolean) {
871+
function setWorkspaceRequiresCategory(
872+
policyID: string,
873+
requiresCategory: boolean,
874+
policyTagLists: PolicyTagLists = {},
875+
allTransactionViolations: OnyxCollection<TransactionViolations> = {},
876+
) {
865877
const onyxData: OnyxData = {
866878
optimisticData: [
867879
{
@@ -907,6 +919,9 @@ function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolea
907919
],
908920
};
909921

922+
pushTransactionViolationsOnyxData(onyxData, policyID, policyTagLists, allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {}, allTransactionViolations, {
923+
requiresCategory,
924+
} as Partial<Policy>);
910925
const parameters = {
911926
policyID,
912927
requiresCategory,
@@ -936,13 +951,22 @@ function clearCategoryErrors(policyID: string, categoryName: string) {
936951
});
937952
}
938953

939-
function deleteWorkspaceCategories(policyID: string, categoryNamesToDelete: string[]) {
954+
function deleteWorkspaceCategories(
955+
policyID: string,
956+
categoryNamesToDelete: string[],
957+
policyTagLists: PolicyTagLists = {},
958+
transactionViolations: OnyxCollection<TransactionViolations> = {},
959+
) {
940960
const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {};
941961
const optimisticPolicyCategoriesData = categoryNamesToDelete.reduce<Record<string, Partial<PolicyCategory>>>((acc, categoryName) => {
942962
acc[categoryName] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, enabled: false};
943963
return acc;
944964
}, {});
945965

966+
const shouldDisableRequiresCategory = !hasEnabledOptions(
967+
Object.values(policyCategories).filter((category) => !categoryNamesToDelete.includes(category.name) && category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE),
968+
);
969+
946970
const onyxData: OnyxData = {
947971
optimisticData: [
948972
{
@@ -977,6 +1001,8 @@ function deleteWorkspaceCategories(policyID: string, categoryNamesToDelete: stri
9771001
],
9781002
};
9791003

1004+
const optimisticPolicyData: Partial<Policy> = shouldDisableRequiresCategory ? {requiresCategory: false} : {};
1005+
pushTransactionViolationsOnyxData(onyxData, policyID, policyTagLists, policyCategories, transactionViolations, optimisticPolicyData, optimisticPolicyCategoriesData);
9801006
appendSetupCategoriesOnboardingData(onyxData);
9811007

9821008
const parameters = {
@@ -987,7 +1013,13 @@ function deleteWorkspaceCategories(policyID: string, categoryNamesToDelete: stri
9871013
API.write(WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES, parameters, onyxData);
9881014
}
9891015

990-
function enablePolicyCategories(policyID: string, enabled: boolean, shouldGoBack = true) {
1016+
function enablePolicyCategories(
1017+
policyID: string,
1018+
enabled: boolean,
1019+
policyTagLists: PolicyTagLists = {},
1020+
allTransactionViolations: OnyxCollection<TransactionViolations> = {},
1021+
shouldGoBack = true,
1022+
) {
9911023
const onyxUpdatesToDisableCategories: OnyxUpdate[] = [];
9921024
if (!enabled) {
9931025
onyxUpdatesToDisableCategories.push(
@@ -1050,6 +1082,22 @@ function enablePolicyCategories(policyID: string, enabled: boolean, shouldGoBack
10501082
],
10511083
};
10521084

1085+
const policyUpdate: Partial<Policy> = {
1086+
areCategoriesEnabled: enabled,
1087+
requiresCategory: enabled,
1088+
pendingFields: {
1089+
areCategoriesEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
1090+
},
1091+
};
1092+
1093+
const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`] ?? {};
1094+
1095+
const policyCategoriesUpdate: Record<string, Partial<PolicyCategory>> = Object.fromEntries(
1096+
Object.entries(policyCategories).map(([categoryName]) => [categoryName, {name: categoryName, enabled}]),
1097+
);
1098+
1099+
pushTransactionViolationsOnyxData(onyxData, policyID, policyTagLists, policyCategories, allTransactionViolations, policyUpdate, policyCategoriesUpdate);
1100+
10531101
if (onyxUpdatesToDisableCategories.length > 0) {
10541102
onyxData.optimisticData?.push(...onyxUpdatesToDisableCategories);
10551103
}

src/pages/iou/request/step/IOURequestStepCategory.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,15 @@ function IOURequestStepCategory({
5858
reportID = reportReal.parentReportID;
5959
}
6060
}
61+
6162
const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {canEvict: false, canBeMissing: true});
6263
const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: true});
6364

6465
const report = reportReal ?? reportDraft;
6566
const policy = policyReal ?? policyDraft;
6667
const policyCategories = policyCategoriesReal ?? policyCategoriesDraft;
68+
const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
69+
const [policyTagLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy?.id}`, {canBeMissing: true});
6770
const {currentSearchHash} = useSearchContext();
6871
const styles = useThemeStyles();
6972
const theme = useTheme();
@@ -192,7 +195,7 @@ function IOURequestStepCategory({
192195
}
193196

194197
if (!policy.areCategoriesEnabled) {
195-
enablePolicyCategories(policy.id, true, false);
198+
enablePolicyCategories(policy.id, true, policyTagLists, allTransactionViolations, false);
196199
}
197200
InteractionManager.runAfterInteractions(() => {
198201
Navigation.navigate(

src/pages/workspace/WorkspaceMoreFeaturesPage.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
100100
const workspaceCards = getAllCardsForWorkspace(workspaceAccountID, cardList, cardFeeds);
101101
const isSmartLimitEnabled = isSmartLimitEnabledUtil(workspaceCards);
102102

103+
const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
104+
const [policyTagLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy?.id}`, {canBeMissing: true});
105+
103106
const onDisabledOrganizeSwitchPress = useCallback(() => {
104107
if (!hasAccountingConnection) {
105108
return;
@@ -248,7 +251,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
248251
if (!policyID) {
249252
return;
250253
}
251-
enablePolicyCategories(policyID, isEnabled, true);
254+
enablePolicyCategories(policyID, isEnabled, policyTagLists, allTransactionViolations, true);
252255
},
253256
},
254257
{

src/pages/workspace/categories/CategorySettingsPage.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ function CategorySettingsPage({
5050
},
5151
navigation,
5252
}: CategorySettingsPageProps) {
53+
const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
54+
const [policyTagLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true});
5355
const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: false});
5456
const styles = useThemeStyles();
5557
const {translate} = useLocalize();
@@ -126,7 +128,7 @@ function CategorySettingsPage({
126128
setIsCannotDeleteOrDisableLastCategoryModalVisible(true);
127129
return;
128130
}
129-
setWorkspaceCategoryEnabled(policyID, {[policyCategory.name]: {name: policyCategory.name, enabled: value}});
131+
setWorkspaceCategoryEnabled(policyID, {[policyCategory.name]: {name: policyCategory.name, enabled: value}}, policyTagLists, allTransactionViolations);
130132
};
131133

132134
const navigateToEditCategory = () => {
@@ -136,7 +138,7 @@ function CategorySettingsPage({
136138
};
137139

138140
const deleteCategory = () => {
139-
deleteWorkspaceCategories(policyID, [categoryName]);
141+
deleteWorkspaceCategories(policyID, [categoryName], policyTagLists, allTransactionViolations);
140142
setDeleteCategoryConfirmModalVisible(false);
141143
navigateBack();
142144
};

src/pages/workspace/categories/WorkspaceCategoriesPage.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
8080
const backTo = route.params?.backTo;
8181
const policy = usePolicy(policyId);
8282
const {selectionMode} = useMobileSelectionMode();
83+
const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
84+
const [policyTagLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyId}`, {canBeMissing: true});
8385
const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyId}`, {canBeMissing: true});
8486
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`, {canBeMissing: true});
8587
const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy);
@@ -115,9 +117,9 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
115117

116118
const updateWorkspaceCategoryEnabled = useCallback(
117119
(value: boolean, categoryName: string) => {
118-
setWorkspaceCategoryEnabled(policyId, {[categoryName]: {name: categoryName, enabled: value}});
120+
setWorkspaceCategoryEnabled(policyId, {[categoryName]: {name: categoryName, enabled: value}}, policyTagLists, allTransactionViolations);
119121
},
120-
[policyId],
122+
[policyId, policyTagLists, allTransactionViolations],
121123
);
122124

123125
const categoryList = useMemo<PolicyOption[]>(() => {
@@ -225,7 +227,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
225227
};
226228

227229
const handleDeleteCategories = () => {
228-
deleteWorkspaceCategories(policyId, selectedCategories);
230+
deleteWorkspaceCategories(policyId, selectedCategories, policyTagLists, allTransactionViolations);
229231
setDeleteCategoriesConfirmModalVisible(false);
230232

231233
InteractionManager.runAfterInteractions(() => {
@@ -329,7 +331,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
329331
return;
330332
}
331333
setSelectedCategories([]);
332-
setWorkspaceCategoryEnabled(policyId, categoriesToDisable);
334+
setWorkspaceCategoryEnabled(policyId, categoriesToDisable, policyTagLists, allTransactionViolations);
333335
},
334336
});
335337
}
@@ -351,7 +353,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
351353
value: CONST.POLICY.BULK_ACTION_TYPES.ENABLE,
352354
onSelected: () => {
353355
setSelectedCategories([]);
354-
setWorkspaceCategoryEnabled(policyId, categoriesToEnable);
356+
setWorkspaceCategoryEnabled(policyId, categoriesToEnable, policyTagLists, allTransactionViolations);
355357
},
356358
});
357359
}

src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,14 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet
4545
const [isSelectorModalVisible, setIsSelectorModalVisible] = useState(false);
4646
const [categoryID, setCategoryID] = useState<string>();
4747
const [groupID, setGroupID] = useState<string>();
48+
const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
49+
const [policyTagLists] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true});
4850
const isQuickSettingsFlow = route.name === SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_SETTINGS;
49-
5051
const toggleSubtitle =
5152
isConnectedToAccounting && currentConnectionName ? translate('workspace.categories.needCategoryForExportToIntegration', {connectionName: currentConnectionName}) : undefined;
5253

5354
const updateWorkspaceRequiresCategory = (value: boolean) => {
54-
setWorkspaceRequiresCategory(policyID, value);
55+
setWorkspaceRequiresCategory(policyID, value, policyTagLists, allTransactionViolations);
5556
};
5657

5758
const {sections} = useMemo(() => {

0 commit comments

Comments
 (0)