diff --git a/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx b/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx index 30e474b46059..1b751e7a9d95 100644 --- a/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx +++ b/src/components/FloatingCameraButton/BaseFloatingCameraButton.tsx @@ -62,7 +62,7 @@ function BaseFloatingCameraButton({icon}: BaseFloatingCameraButtonProps) { interceptAnonymousUser(() => { if ( policyChatForActivePolicy?.policyID && - shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed) + shouldRestrictUserBillableActions(policyChatForActivePolicy.policyID, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, activePolicy) ) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policyChatForActivePolicy.policyID)); return; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 8f14fc1b6994..f8d024e393ae 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -2188,7 +2188,7 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa if (!moneyRequestReport?.reportID) { return; } - if (policy && shouldRestrictUserBillableActions(policy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed)) { + if (policy && shouldRestrictUserBillableActions(policy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, policy)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); return; } diff --git a/src/components/MoneyRequestReportView/SearchMoneyRequestReportEmptyState.tsx b/src/components/MoneyRequestReportView/SearchMoneyRequestReportEmptyState.tsx index a6255a472461..2f7f89548767 100644 --- a/src/components/MoneyRequestReportView/SearchMoneyRequestReportEmptyState.tsx +++ b/src/components/MoneyRequestReportView/SearchMoneyRequestReportEmptyState.tsx @@ -44,7 +44,7 @@ function SearchMoneyRequestReportEmptyState({report, policy, onLayout}: {report: if (!reportId) { return; } - if (policy && shouldRestrictUserBillableActions(policy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed)) { + if (policy && shouldRestrictUserBillableActions(policy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, policy)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); return; } @@ -59,7 +59,7 @@ function SearchMoneyRequestReportEmptyState({report, policy, onLayout}: {report: if (!reportId) { return; } - if (policy && shouldRestrictUserBillableActions(policy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed)) { + if (policy && shouldRestrictUserBillableActions(policy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, policy)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); return; } @@ -71,7 +71,7 @@ function SearchMoneyRequestReportEmptyState({report, policy, onLayout}: {report: text: translate('iou.addUnreportedExpense'), icon: icons.ReceiptPlus, onSelected: () => { - if (policy && shouldRestrictUserBillableActions(policy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed)) { + if (policy && shouldRestrictUserBillableActions(policy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, policy)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); return; } diff --git a/src/components/Navigation/QuickCreationActionsBar/index.tsx b/src/components/Navigation/QuickCreationActionsBar/index.tsx index 1804bfdacbe7..f9abff7d65b9 100644 --- a/src/components/Navigation/QuickCreationActionsBar/index.tsx +++ b/src/components/Navigation/QuickCreationActionsBar/index.tsx @@ -175,13 +175,14 @@ function QuickCreationActionsBar() { if ( !workspaceIDForReportCreation || - (shouldRestrictUserBillableActions(workspaceIDForReportCreation, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds) && groupPoliciesWithChatEnabled.length > 1) + (shouldRestrictUserBillableActions(workspaceIDForReportCreation, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, undefined, defaultChatEnabledPolicy) && + groupPoliciesWithChatEnabled.length > 1) ) { Navigation.navigate(ROUTES.NEW_REPORT_WORKSPACE_SELECTION.getRoute()); return; } - if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds)) { + if (!shouldRestrictUserBillableActions(workspaceIDForReportCreation, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, undefined, defaultChatEnabledPolicy)) { if (shouldShowEmptyReportConfirmationForDefaultChatEnabledPolicy) { openCreateReportConfirmation(); } else { @@ -199,6 +200,7 @@ function QuickCreationActionsBar() { defaultChatEnabledPolicyID, userBillingGracePeriodEnds, ownerBillingGracePeriodEnd, + defaultChatEnabledPolicy, groupPoliciesWithChatEnabled.length, shouldShowEmptyReportConfirmationForDefaultChatEnabledPolicy, openCreateReportConfirmation, diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index e74c89567fb9..ff1d2c115179 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -222,7 +222,7 @@ function SettlementButton({ return true; } - if (policy && shouldRestrictUserBillableActions(policy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed)) { + if (policy && shouldRestrictUserBillableActions(policy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, policy)) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(policy.id)); return true; } diff --git a/src/hooks/useReceiptScanDrop.tsx b/src/hooks/useReceiptScanDrop.tsx index 0906273e4971..2fc2b05113ad 100644 --- a/src/hooks/useReceiptScanDrop.tsx +++ b/src/hooks/useReceiptScanDrop.tsx @@ -81,7 +81,7 @@ function useReceiptScanDrop() { if ( isPaidGroupPolicy(activePolicy) && activePolicy?.isPolicyExpenseChatEnabled && - !shouldRestrictUserBillableActions(activePolicy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed) + !shouldRestrictUserBillableActions(activePolicy.id, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, amountOwed, activePolicy) ) { const shouldAutoReport = !!activePolicy?.autoReporting || !!personalPolicy?.autoReporting; const report = shouldAutoReport ? getPolicyExpenseChat(currentUserPersonalDetails.accountID, activePolicy?.id) : selfDMReport; diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 76918008007d..b9a56e5f5f30 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -56,7 +56,7 @@ import {canIOUBePaid, dismissRejectUseExplanation, initBulkEditDraftTransaction} import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {BillingGraceEndPeriod, Report, SearchResults, Transaction, TransactionViolations} from '@src/types/onyx'; +import type {BillingGraceEndPeriod, Policy, Report, SearchResults, Transaction, TransactionViolations} from '@src/types/onyx'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import useAllTransactions from './useAllTransactions'; import useBulkPayOptions from './useBulkPayOptions'; @@ -85,10 +85,15 @@ function getRestrictedPolicyID( billingGracePeriods: OnyxCollection, ownerBillingGracePeriodEnd: OnyxEntry, amountOwed: OnyxEntry, + allPolicies: OnyxCollection, ): string | undefined { return items .map((item) => item.policyID) - .find((policyID): policyID is string => !!policyID && shouldRestrictUserBillableActions(policyID, ownerBillingGracePeriodEnd, billingGracePeriods, amountOwed)); + .find( + (policyID): policyID is string => + !!policyID && + shouldRestrictUserBillableActions(policyID, ownerBillingGracePeriodEnd, billingGracePeriods, amountOwed, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]), + ); } function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { @@ -400,7 +405,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { const selectedItems = selectedReports.length ? selectedReports : Object.values(selectedTransactions); - const restrictedPolicyID = getRestrictedPolicyID(selectedItems, userBillingGracePeriodEnds, ownerBillingGracePeriodEnd, amountOwed); + const restrictedPolicyID = getRestrictedPolicyID(selectedItems, userBillingGracePeriodEnds, ownerBillingGracePeriodEnd, amountOwed, policies); if (restrictedPolicyID) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(restrictedPolicyID)); return; @@ -428,6 +433,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { userBillingGracePeriodEnds, ownerBillingGracePeriodEnd, amountOwed, + policies, ]); const {expenseCount, uniqueReportCount} = useMemo(() => { @@ -557,7 +563,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { const selectedOptions = selectedReports.length ? selectedReports : Object.values(selectedTransactions); const expenseReportBankAccountID = additionalData?.bankAccountID; - const restrictedPolicyID = getRestrictedPolicyID(selectedOptions, userBillingGracePeriodEnds, ownerBillingGracePeriodEnd, amountOwed); + const restrictedPolicyID = getRestrictedPolicyID(selectedOptions, userBillingGracePeriodEnds, ownerBillingGracePeriodEnd, amountOwed, policies); if (restrictedPolicyID) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(restrictedPolicyID)); return; @@ -1018,7 +1024,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { const itemList = !selectedReports.length ? Object.values(selectedTransactions).map((transaction) => transaction) : (selectedReports?.filter((report) => !!report) ?? []); - const restrictedPolicyID = getRestrictedPolicyID(itemList, userBillingGracePeriodEnds, ownerBillingGracePeriodEnd, amountOwed); + const restrictedPolicyID = getRestrictedPolicyID(itemList, userBillingGracePeriodEnds, ownerBillingGracePeriodEnd, amountOwed, policies); if (restrictedPolicyID) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(restrictedPolicyID)); return; diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 1b4cd1f262a8..afca7d213481 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -460,11 +460,10 @@ function shouldRestrictUserBillableActions( ownerBillingGracePeriodEnd: OnyxEntry, userBillingGracePeriodEnds: OnyxCollection, amountOwed: OnyxEntry = privateAmountOwed, + policy: OnyxEntry = deprecatedAllPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`], ): boolean { const currentDate = new Date(); - const policy = deprecatedAllPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; - // This logic will be executed if the user is a workspace's non-owner (normal user or admin). // We should restrict the workspace's non-owner actions if it's member of a workspace where the owner is // past due and is past its grace period end. diff --git a/src/pages/AddUnreportedExpense.tsx b/src/pages/AddUnreportedExpense.tsx index 2c266b392a1e..dd0aad315340 100644 --- a/src/pages/AddUnreportedExpense.tsx +++ b/src/pages/AddUnreportedExpense.tsx @@ -296,7 +296,11 @@ function AddUnreportedExpense({route}: AddUnreportedExpensePageType) { { buttonText: translate('iou.createExpense'), buttonAction: () => { - if (report && report.policyID && shouldRestrictUserBillableActions(report.policyID, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds)) { + if ( + report && + report.policyID && + shouldRestrictUserBillableActions(report.policyID, ownerBillingGracePeriodEnd, userBillingGracePeriodEnds, undefined, policy) + ) { Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(report.policyID)); return; } diff --git a/tests/unit/SubscriptionUtilsTest.ts b/tests/unit/SubscriptionUtilsTest.ts index 34994ef3a51d..73486023b786 100644 --- a/tests/unit/SubscriptionUtilsTest.ts +++ b/tests/unit/SubscriptionUtilsTest.ts @@ -522,6 +522,122 @@ describe('SubscriptionUtils', () => { }); }); + describe('shouldRestrictUserBillableActions - policy parameter', () => { + afterEach(async () => { + await Onyx.clear(); + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: null, + [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: null, + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: null, + [ONYXKEYS.COLLECTION.POLICY]: null, + }); + }); + + it('should restrict when policy is passed directly and owner is past due', async () => { + const accountID = 1; + const policyID = '2001'; + const policy = { + ...createRandomPolicy(Number(policyID)), + ownerAccountID: accountID, + }; + + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: '', accountID}, + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: 8010, + }); + + expect(shouldRestrictUserBillableActions(policyID, getUnixTime(subDays(new Date(), 3)), undefined, undefined, policy)).toBeTruthy(); + }); + + it('should not restrict when policy is passed directly but owner is not past due', async () => { + const accountID = 1; + const policyID = '2001'; + const policy = { + ...createRandomPolicy(Number(policyID)), + ownerAccountID: accountID, + }; + + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: '', accountID}, + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: 8010, + }); + + expect(shouldRestrictUserBillableActions(policyID, getUnixTime(addDays(new Date(), 3)), undefined, undefined, policy)).toBeFalsy(); + }); + + it('should not restrict when policy is passed as undefined', () => { + expect(shouldRestrictUserBillableActions('nonexistent', getUnixTime(subDays(new Date(), 3)), undefined, 500, undefined)).toBeFalsy(); + }); + + it('should restrict for non-owner when policy is passed directly and billing grace period is overdue', async () => { + const policyID = '2001'; + const ownerAccountID = 2001; + const policy = { + ...createRandomPolicy(Number(policyID)), + ownerAccountID, + }; + + expect( + shouldRestrictUserBillableActions( + policyID, + undefined, + { + [`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END}${ownerAccountID}` as const]: { + ...billingGraceEndPeriod, + value: getUnixTime(subDays(new Date(), 3)), + }, + }, + undefined, + policy, + ), + ).toBeTruthy(); + }); + + it('should not restrict for non-owner when policy is passed directly but billing grace period is not overdue', async () => { + const policyID = '2001'; + const ownerAccountID = 2001; + const policy = { + ...createRandomPolicy(Number(policyID)), + ownerAccountID, + }; + + expect( + shouldRestrictUserBillableActions( + policyID, + undefined, + { + [`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END}${ownerAccountID}` as const]: { + ...billingGraceEndPeriod, + value: getUnixTime(addDays(new Date(), 3)), + }, + }, + undefined, + policy, + ), + ).toBeFalsy(); + }); + + it('should use passed policy and ignore Onyx-stored policies', async () => { + const accountID = 1; + const policyID = '2001'; + const differentOwnerPolicy = { + ...createRandomPolicy(Number(policyID)), + ownerAccountID: 9999, + }; + + await Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: '', accountID}, + [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: 8010, + [`${ONYXKEYS.COLLECTION.POLICY}${policyID}` as const]: { + ...createRandomPolicy(Number(policyID)), + ownerAccountID: accountID, + }, + }); + + expect(shouldRestrictUserBillableActions(policyID, getUnixTime(subDays(new Date(), 3)), undefined, undefined, differentOwnerPolicy)).toBeFalsy(); + }); + }); + describe('getSubscriptionStatus', () => { afterEach(async () => { await Onyx.clear();