Skip to content

Commit 0f3fb24

Browse files
Add shouldRestrictUserBillableActions guard to duplicate expense handlers
The duplicate expense flow never checks whether the target workspace has expired billing. This allows users to bypass the billing restriction by duplicating expenses into expired workspaces. Add the same shouldRestrictUserBillableActions guard that exists in the Add Expense handler to all duplicate expense entry points: - useExpenseActions: DUPLICATE_EXPENSE and DUPLICATE_REPORT handlers - MoneyRequestHeaderSecondaryActions: transaction-level DUPLICATE handler - useSearchBulkActions: bulk DUPLICATE handler (both contexts) Co-authored-by: Eugene Voloshchak <eVoloshchak@users.noreply.github.com>
1 parent 45d6ada commit 0f3fb24

3 files changed

Lines changed: 49 additions & 3 deletions

File tree

src/components/MoneyRequestHeaderSecondaryActions.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import {
5050
navigateToDetailsPage,
5151
rejectMoneyRequestReason,
5252
} from '@libs/ReportUtils';
53+
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
5354
import {
5455
getOriginalTransactionWithSplitInfo,
5556
hasCustomUnitOutOfPolicyViolation as hasCustomUnitOutOfPolicyViolationTransactionUtils,
@@ -138,6 +139,9 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
138139
const [recentWaypoints] = useOnyx(ONYXKEYS.NVP_RECENT_WAYPOINTS);
139140
const [shouldFailAllRequests] = useOnyx(ONYXKEYS.NETWORK, {selector: shouldFailAllRequestsSelector});
140141
const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE);
142+
const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
143+
const [userBillingGracePeriodEnds] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
144+
const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED);
141145
const [isSelfTourViewed = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
142146
const [betas] = useOnyx(ONYXKEYS.BETAS);
143147

@@ -350,6 +354,14 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
350354
iconFill: isDuplicateActive ? undefined : theme.icon,
351355
value: CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.DUPLICATE,
352356
onSelected: () => {
357+
if (
358+
defaultExpensePolicy &&
359+
shouldRestrictUserBillableActions(defaultExpensePolicy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, defaultExpensePolicy)
360+
) {
361+
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(defaultExpensePolicy.id));
362+
return;
363+
}
364+
353365
if (hasCustomUnitOutOfPolicyViolation) {
354366
showConfirmModal({
355367
title: translate('common.duplicateExpense'),

src/hooks/useExpenseActions.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,14 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic
317317
iconFill: isDuplicateActive ? undefined : theme.icon,
318318
value: CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE_EXPENSE,
319319
onSelected: () => {
320+
if (
321+
defaultExpensePolicy &&
322+
shouldRestrictUserBillableActions(defaultExpensePolicy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, defaultExpensePolicy)
323+
) {
324+
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(defaultExpensePolicy.id));
325+
return;
326+
}
327+
320328
if (hasCustomUnitOutOfPolicyViolation) {
321329
showConfirmModal({
322330
title: translate('common.duplicateExpense'),
@@ -369,11 +377,20 @@ function useExpenseActions({reportID, isReportInSearch = false, backTo, onDuplic
369377
return;
370378
}
371379

380+
const isSourcePolicyValid = !!policy && isPolicyAccessible(policy, currentUserLogin ?? '');
381+
const targetPolicyForDuplicate = isSourcePolicyValid ? policy : defaultExpensePolicy;
382+
383+
if (
384+
targetPolicyForDuplicate &&
385+
shouldRestrictUserBillableActions(targetPolicyForDuplicate.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, targetPolicyForDuplicate)
386+
) {
387+
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(targetPolicyForDuplicate.id));
388+
return;
389+
}
390+
372391
temporarilyDisableDuplicateReportAction();
373392
wasDuplicateReportTriggeredRef.current = true;
374393

375-
const isSourcePolicyValid = !!policy && isPolicyAccessible(policy, currentUserLogin ?? '');
376-
const targetPolicyForDuplicate = isSourcePolicyValid ? policy : defaultExpensePolicy;
377394
const targetChatForDuplicate = isSourcePolicyValid ? chatReport : activePolicyExpenseChat;
378395
const activePolicyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${targetPolicyForDuplicate?.id}`] ?? {};
379396

src/hooks/useSearchBulkActions.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1056,7 +1056,16 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
10561056
icon: expensifyIcons.ExpenseCopy,
10571057
value: CONST.SEARCH.BULK_ACTION_TYPES.DUPLICATE,
10581058
shouldCloseModalOnSelect: true,
1059-
onSelected: invokeDuplicateHandler,
1059+
onSelected: () => {
1060+
if (
1061+
defaultExpensePolicy &&
1062+
shouldRestrictUserBillableActions(defaultExpensePolicy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, defaultExpensePolicy)
1063+
) {
1064+
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(defaultExpensePolicy.id));
1065+
return;
1066+
}
1067+
invokeDuplicateHandler();
1068+
},
10601069
});
10611070
}
10621071

@@ -1368,6 +1377,13 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
13681377
value: CONST.SEARCH.BULK_ACTION_TYPES.DUPLICATE,
13691378
shouldCloseModalOnSelect: true,
13701379
onSelected: () => {
1380+
if (
1381+
defaultExpensePolicy &&
1382+
shouldRestrictUserBillableActions(defaultExpensePolicy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, defaultExpensePolicy)
1383+
) {
1384+
Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(defaultExpensePolicy.id));
1385+
return;
1386+
}
13711387
if (exceedsBulkDuplicateLimit) {
13721388
showConfirmModal({
13731389
title: translate('common.duplicateExpense'),
@@ -1477,6 +1493,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) {
14771493
allTransactions,
14781494
isBetaEnabled,
14791495
shouldShowBusinessBankAccountOptions,
1496+
defaultExpensePolicy,
14801497
]);
14811498

14821499
const handleOfflineModalClose = useCallback(() => {

0 commit comments

Comments
 (0)