diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index a4240970d8a6..60670ebfb25b 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -752,6 +752,7 @@ function getSuggestedSearchesVisibility( const isPayer = isPolicyPayer(policy, currentUserEmail); const isAdmin = policy.role === CONST.POLICY.ROLE.ADMIN; const isAuditor = policy.role === CONST.POLICY.ROLE.AUDITOR; + const isPaymentsAdmin = policy.role === CONST.POLICY.ROLE.PAYMENTS_ADMIN; const isExporter = policy.exporter === currentUserEmail; const isSubmittedTo = @@ -772,10 +773,10 @@ function getSuggestedSearchesVisibility( const isPolicyEligibleForApproveSuggestion = isGroupPolicyEligible && isEligibleForApproveSuggestion(policy.approvalMode, isUserApprover, isSubmittedTo); const isEligibleForExportSuggestion = isExporter && !hasExportError; const isEligibleForStatementsSuggestion = isPaidPolicy && (hasCardFeed || !!defaultExpensifyCard); - const isEligibleForUnapprovedCashSuggestion = isPaidPolicy && (isAdmin || isAuditor) && isApprovalEnabled && isPaymentEnabled; + const isEligibleForUnapprovedCashSuggestion = isPaidPolicy && (isAdmin || isAuditor || isPaymentsAdmin) && isApprovalEnabled && isPaymentEnabled; const isEligibleForUnapprovedCardSuggestion = isPaidPolicy && (isAdmin || isAuditor) && isApprovalEnabled && (hasCardFeed || !!defaultExpensifyCard); const isEligibleForExpensifyCardSuggestion = isPaidPolicy && (isAdmin || isAuditor) && isECardEnabled; - const isEligibleForReimbursementsSuggestion = isPaidPolicy && (isAdmin || isAuditor) && isPaymentEnabled && hasVBBA && hasReimburser; + const isEligibleForReimbursementsSuggestion = isPaidPolicy && (isAdmin || isAuditor || isPaymentsAdmin) && isPaymentEnabled && hasVBBA && hasReimburser; const memberCount = Object.keys(policy.employeeList ?? {}).length; const isEligibleForTopSpendersSuggestion = isPolicyEligibleForTopSpenders(policy, currentUserEmail); const isEligibleForTopCategoriesSuggestion = isPolicyEligibleForTopCategories(policy); diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 801607d094d9..c2e3ee7dd16d 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -10115,6 +10115,90 @@ describe('SearchUIUtils', () => { expect(reconciliationItem?.searchQuery).toContain('withdrawalType:reimbursement'); }); + it('should show Cash accruals and Bank reconciliation, but no Card searches, to a Payments Admin', () => { + // Given a Payments Admin on a workspace with approvals, ACH reimbursement, Expensify Cards, and a card feed + const paymentsAdminEmail = 'paymentsadmin@policy.com'; + const mockPolicies = { + policy1: { + id: 'policy1', + name: 'Payments Admin Policy', + owner: adminEmail, + outputCurrency: 'USD', + role: CONST.POLICY.ROLE.PAYMENTS_ADMIN, + type: CONST.POLICY.TYPE.CORPORATE, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + areExpensifyCardsEnabled: true, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, + achAccount: { + bankAccountID: 1234, + reimburser: adminEmail, + state: CONST.BANK_ACCOUNT.STATE.OPEN, + accountNumber: '1234567890', + routingNumber: '1234567890', + addressName: 'Test Address', + bankName: 'Test Bank', + }, + }, + }; + const mockCardFeedsByPolicy: Record = { + policy1: [ + { + id: 'card1', + feed: 'Expensify Card' as const, + fundID: 'fund1', + name: 'Test Card Feed', + }, + ], + }; + + // When the suggested searches are computed for that Payments Admin + const {visibility, shouldShowExpensifyCard} = SearchUIUtils.getSuggestedSearchesVisibility(paymentsAdminEmail, mockCardFeedsByPolicy, mockPolicies, undefined); + const sections = SearchUIUtils.createTypeMenuSections({ + currentUserEmail: paymentsAdminEmail, + currentUserAccountID: adminAccountID, + cardFeedsByPolicy: mockCardFeedsByPolicy, + defaultCardFeed: undefined, + policies: mockPolicies, + savedSearches: {}, + isOffline: false, + defaultExpensifyCard: undefined, + draftTransactionIDs: [], + isTrackIntentUser: false, + }); + const reconciliationItem = sections.flatMap((section) => section.menuItems).find((item) => item.key === CONST.SEARCH.SEARCH_KEYS.RECONCILIATION); + + // Then the role gets the reimbursement searches, while Card accruals and Card reconciliation stay limited to Admins and Auditors + expect(visibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]).toBe(true); + expect(visibility[CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]).toBe(true); + expect(visibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CARD]).toBe(false); + expect(shouldShowExpensifyCard).toBe(false); + expect(reconciliationItem?.searchQuery).toContain('withdrawalType:reimbursement'); + }); + + it('should hide Bank reconciliation from a Payments Admin when the workspace has no active business bank account', () => { + // Given a Payments Admin on a workspace that pays without a verified business bank account + const paymentsAdminEmail = 'paymentsadmin@policy.com'; + const mockPolicies = { + policy1: { + id: 'policy1', + name: 'Payments Admin Policy', + owner: adminEmail, + outputCurrency: 'USD', + role: CONST.POLICY.ROLE.PAYMENTS_ADMIN, + type: CONST.POLICY.TYPE.CORPORATE, + approvalMode: CONST.POLICY.APPROVAL_MODE.ADVANCED, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, + }, + }; + + // When the suggested searches are computed for that Payments Admin + const {visibility} = SearchUIUtils.getSuggestedSearchesVisibility(paymentsAdminEmail, {}, mockPolicies, undefined); + + // Then Cash accruals still shows, but Bank reconciliation needs a bank account to reconcile against + expect(visibility[CONST.SEARCH.SEARCH_KEYS.UNAPPROVED_CASH]).toBe(true); + expect(visibility[CONST.SEARCH.SEARCH_KEYS.RECONCILIATION]).toBe(false); + }); + it('should place todo items (submit, approve, pay) inside the Expense reports section', () => { const mockPolicies = { policy1: {