diff --git a/src/hooks/useResetBankAccountModal.tsx b/src/hooks/useResetBankAccountModal.tsx index b198b3f4d8d3..f591268b753f 100644 --- a/src/hooks/useResetBankAccountModal.tsx +++ b/src/hooks/useResetBankAccountModal.tsx @@ -60,7 +60,7 @@ function useResetBankAccountModal({ const achData = reimbursementAccount?.achData; const shouldShowResetModal = reimbursementAccount?.shouldShowResetModal ?? false; const isInOpenState = achData?.state === CONST.BANK_ACCOUNT.STATE.OPEN; - const bankAccountID = achData?.bankAccountID; + const bankAccountID = achData?.bankAccountID ?? policy?.achAccount?.bankAccountID; const bankShortName = `${achData?.addressName ?? ''} ${(achData?.accountNumber ?? '').slice(-4)}`; const lastPaymentMethodSelector = useCallback( @@ -77,7 +77,7 @@ function useResetBankAccountModal({ const handleConfirm = () => { if (isNonUSDWorkspace) { - resetNonUSDBankAccount(policyID, policy?.achAccount, achData?.bankAccountID, lastPaymentMethod); + resetNonUSDBankAccount(policyID, policy?.achAccount, bankAccountID, lastPaymentMethod, policy?.owner); if (setShouldShowConnectedVerifiedBankAccount) { setShouldShowConnectedVerifiedBankAccount(false); @@ -91,7 +91,7 @@ function useResetBankAccountModal({ ROUTES.BANK_ACCOUNT_NON_USD_SETUP.getRoute({policyID: policyID ?? CONST.POLICY.ID_FAKE, page: CONST.NON_USD_BANK_ACCOUNT.PAGE_NAME.CURRENCY_AND_COUNTRY, backTo}), ); } else { - resetUSDBankAccount(bankAccountID, session, policyID, policy?.achAccount, lastPaymentMethod); + resetUSDBankAccount(bankAccountID, session, policyID, policy?.achAccount, lastPaymentMethod, policy?.owner); if (setShouldShowContinueSetupButton) { setShouldShowContinueSetupButton(false); diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index bb3fc9717c5f..d4d4ada4b9ac 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -595,17 +595,22 @@ function isPolicyPayer(policy: OnyxEntry, currentUserLogin: string | und } const isAdmin = policy.role === CONST.POLICY.ROLE.ADMIN; - const isReimburser = policy.reimburser === currentUserLogin; + const isAutoReimbursement = policy.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES; + const isManualReimbursement = policy.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL; - if (policy.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES) { - return policy.reimburser ? isReimburser : isAdmin; + // Reimbursement is disabled for this workspace. + if (!isAutoReimbursement && !isManualReimbursement) { + return false; } - if (policy.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL) { + const reimburserEmail = policy.achAccount?.reimburser ?? (isManualReimbursement ? policy.owner : undefined); + + // No designated reimburser means any workspace admin can pay. + if (!reimburserEmail) { return isAdmin; } - return false; + return currentUserLogin === reimburserEmail; } /** Check if the passed employee is an approver in the policy's employeeList */ diff --git a/src/libs/ReportPreviewActionUtils.ts b/src/libs/ReportPreviewActionUtils.ts index e0187355c142..29bfa0959443 100644 --- a/src/libs/ReportPreviewActionUtils.ts +++ b/src/libs/ReportPreviewActionUtils.ts @@ -8,6 +8,7 @@ import { getValidConnectedIntegration, hasDynamicExternalWorkflow, hasIntegrationAutoSync, + isPolicyAdmin, isPreferredExporter, isSubmitterApproveBlockedOnSubmitWorkspace, } from './PolicyUtils'; @@ -120,6 +121,7 @@ function canPay( } const isReportPayer = isPayer(currentUserAccountID, currentUserLogin, report, bankAccountList, policy, false); + const canPayReport = isReportPayer || (policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL && isPolicyAdmin(policy)); const isExpense = isExpenseReport(report); const isPaymentsEnabled = arePaymentsEnabled(policy); const isProcessing = isProcessingReport(report); @@ -137,7 +139,7 @@ function canPay( if ( isExpense && - isReportPayer && + canPayReport && isPaymentsEnabled && isReportFinished && (reimbursableSpend !== 0 || (nonReimbursableSpend !== 0 && hasOnlyNonReimbursableTransactions(report?.reportID, transactions))) @@ -151,7 +153,7 @@ function canPay( const isIOU = isIOUReport(report); - if (isIOU && isReportPayer && !isReimbursed && reimbursableSpend > 0) { + if (isIOU && canPayReport && !isReimbursed && reimbursableSpend > 0) { return true; } diff --git a/src/libs/ReportPrimaryActionUtils.ts b/src/libs/ReportPrimaryActionUtils.ts index be2e8682ad76..fa25fc30c970 100644 --- a/src/libs/ReportPrimaryActionUtils.ts +++ b/src/libs/ReportPrimaryActionUtils.ts @@ -92,6 +92,7 @@ type IsPrimaryPayActionParams = { invoiceReceiverPolicy?: Policy; reportActions?: ReportAction[]; isSecondaryAction?: boolean; + canNonPayerAdminPay?: boolean; }; function isAddExpenseAction(report: Report, reportTransactions: Transaction[], isChatReportArchived: boolean) { @@ -207,6 +208,7 @@ function isPrimaryPayAction({ invoiceReceiverPolicy, reportActions, isSecondaryAction, + canNonPayerAdminPay, }: IsPrimaryPayActionParams) { if (isArchivedReport(reportNameValuePairs) || isChatReportArchived) { return false; @@ -216,6 +218,8 @@ function isPrimaryPayAction({ return false; } const isReportPayer = isPayer(currentUserAccountID, currentUserLogin, report, bankAccountList, policy, false); + const canPayReport = + isReportPayer || (canNonPayerAdminPay && policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL && isPolicyAdminPolicyUtils(policy)); const arePaymentsEnabled = arePaymentsEnabledUtils(policy); const isReportApproved = isReportApprovedUtils({report}); const isReportClosed = isClosedReportUtils(report); @@ -231,7 +235,7 @@ function isPrimaryPayAction({ const {reimbursableSpend, nonReimbursableSpend} = getMoneyRequestSpendBreakdown(report); if ( - isReportPayer && + canPayReport && isExpenseReport && arePaymentsEnabled && isReportFinished && @@ -246,7 +250,7 @@ function isPrimaryPayAction({ const isIOUReport = isIOUReportUtils(report); - if (isIOUReport && isReportPayer && reimbursableSpend > 0) { + if (isIOUReport && canPayReport && reimbursableSpend > 0) { return true; } @@ -533,6 +537,7 @@ function getReportPrimaryAction(params: GetReportPrimaryActionParams): ValueOf 0) { + if (isIOU && canPay && !iouSettled && reimbursableSpend > 0) { return true; } @@ -243,7 +244,7 @@ function canIOUBePaid( } return ( - isPayer && + canPay && isReportFinished && !iouSettled && (reimbursableSpend > 0 || canShowMarkedAsPaidForNegativeAmount || isOnlyNonReimbursablePayElsewhere) && @@ -297,15 +298,18 @@ function getBadgeFromIOUReport( currentUserLogin: string, currentUserAccountID: number, ): ValueOf | undefined { - // Show to the actual payer, or to policy admins via the pay-elsewhere path for negative expenses - const canBePaidNow = canIOUBePaid(iouReport, chatReport, policy, undefined, currentUserLogin, currentUserAccountID, undefined, undefined, undefined, invoiceReceiverPolicy); + const isReportPayer = isPayerReportUtils(currentUserAccountID, currentUserLogin, iouReport, undefined, policy, false); + const canBePaidNow = + isReportPayer && canIOUBePaid(iouReport, chatReport, policy, undefined, currentUserLogin, currentUserAccountID, undefined, undefined, undefined, invoiceReceiverPolicy); if (canBePaidNow) { return CONST.REPORT.ACTION_BADGE.PAY; } // Pay-elsewhere path: covers negative reimbursable spend (mark-as-paid flow for credits). // Skip the PAY badge when every expense is non-reimbursable — paying is optional and // should not pin the report in the LHN. - const canBePaidElsewhere = canIOUBePaid(iouReport, chatReport, policy, undefined, currentUserLogin, currentUserAccountID, undefined, true, undefined, invoiceReceiverPolicy); + const canPayElsewhereActor = isPayerReportUtils(currentUserAccountID, currentUserLogin, iouReport, undefined, policy, true); + const canBePaidElsewhere = + canPayElsewhereActor && canIOUBePaid(iouReport, chatReport, policy, undefined, currentUserLogin, currentUserAccountID, undefined, true, undefined, invoiceReceiverPolicy); if (canBePaidElsewhere) { return hasOnlyNonReimbursableTransactions(iouReport?.reportID) ? undefined : CONST.REPORT.ACTION_BADGE.PAY; } diff --git a/src/libs/actions/ReimbursementAccount/resetNonUSDBankAccount.ts b/src/libs/actions/ReimbursementAccount/resetNonUSDBankAccount.ts index 2921f6229155..bd2b770d2c91 100644 --- a/src/libs/actions/ReimbursementAccount/resetNonUSDBankAccount.ts +++ b/src/libs/actions/ReimbursementAccount/resetNonUSDBankAccount.ts @@ -8,7 +8,15 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {ACHAccount} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; -function resetNonUSDBankAccount(policyID: string | undefined, achAccount: OnyxEntry, bankAccountID?: number, lastUsedPaymentMethod?: OnyxTypes.LastPaymentMethodType) { +function resetNonUSDBankAccount( + policyID: string | undefined, + achAccount: OnyxEntry, + bankAccountID?: number, + lastUsedPaymentMethod?: OnyxTypes.LastPaymentMethodType, + policyOwner?: string, +) { + const reimburserEmail = achAccount?.reimburser ?? policyOwner; + // If there's no bankAccountID, we reset locally without making an API call if (!bankAccountID) { const updateData: Array> = [ @@ -21,7 +29,18 @@ function resetNonUSDBankAccount(policyID: string | undefined, achAccount: OnyxEn onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - achAccount: null, + achAccount: reimburserEmail + ? { + reimburser: reimburserEmail, + bankAccountID: null, + accountNumber: null, + routingNumber: null, + addressName: null, + bankName: null, + state: null, + sharees: null, + } + : null, }, }, { @@ -97,7 +116,18 @@ function resetNonUSDBankAccount(policyID: string | undefined, achAccount: OnyxEn onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - achAccount: null, + achAccount: reimburserEmail + ? { + reimburser: reimburserEmail, + bankAccountID: null, + accountNumber: null, + routingNumber: null, + addressName: null, + bankName: null, + state: null, + sharees: null, + } + : null, }, }); diff --git a/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts b/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts index b1b3a6df7190..831c1e041a95 100644 --- a/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts +++ b/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts @@ -18,6 +18,7 @@ function resetUSDBankAccount( policyID: string | undefined, achAccount: ACHAccount | undefined, lastUsedPaymentMethod?: OnyxTypes.LastPaymentMethodType, + policyOwner?: string, ) { if (!bankAccountID) { throw new Error('Missing bankAccountID when attempting to reset free plan bank account'); @@ -28,6 +29,7 @@ function resetUSDBankAccount( const isLastUsedPaymentMethodBBA = lastUsedPaymentMethod?.expense?.name === CONST.IOU.PAYMENT_TYPE.VBBA; const isPreviousLastUsedPaymentMethodBBA = lastUsedPaymentMethod?.lastUsed?.name === CONST.IOU.PAYMENT_TYPE.VBBA; + const reimburserEmail = achAccount?.reimburser ?? policyOwner; const onyxData: OnyxData< | typeof ONYXKEYS.REIMBURSEMENT_ACCOUNT @@ -55,7 +57,18 @@ function resetUSDBankAccount( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - achAccount: null, + achAccount: reimburserEmail + ? { + reimburser: reimburserEmail, + bankAccountID: null, + accountNumber: null, + routingNumber: null, + addressName: null, + bankName: null, + state: null, + sharees: null, + } + : null, }, }, { diff --git a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx index 23a8229f2905..695128362d40 100644 --- a/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx +++ b/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx @@ -262,7 +262,9 @@ function WorkspaceUpgradePage({route}: WorkspaceUpgradePageProps) { break; case CONST.UPGRADE_FEATURE_INTRO_MAPPING.payments.id: { let newReimbursementChoice; - if (!!policy?.achAccount && !isCurrencySupportedForDirectReimbursement(policy?.outputCurrency ?? '')) { + const hasConnectedBank = + policy?.achAccount?.bankAccountID && (policy.achAccount.state === CONST.BANK_ACCOUNT.STATE.OPEN || policy.achAccount.state === CONST.BANK_ACCOUNT.STATE.LOCKED); + if (!hasConnectedBank || !isCurrencySupportedForDirectReimbursement(policy?.outputCurrency ?? '')) { newReimbursementChoice = CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL; } else { newReimbursementChoice = CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES; diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index ec3947ea78bb..97f379306479 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -170,7 +170,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const delegateAccountID = useDelegateAccountID(); const {accountID: currentUserAccountID, email: currentUserEmail = '', login: currentUserLogin = ''} = useCurrentUserPersonalDetails(); - const isUserReimburser = policy?.achAccount?.reimburser !== undefined && account?.primaryLogin !== undefined && policy?.achAccount?.reimburser === account?.primaryLogin; + const isUserReimburser = account?.primaryLogin !== undefined && (policy?.achAccount?.reimburser ?? policy?.owner) === account?.primaryLogin; const {approvalWorkflows, availableMembers, usedApproverEmails} = useMemo( () => convertPolicyEmployeesToApprovalWorkflows({ @@ -187,10 +187,8 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { const isAdvanceApproval = (approvalWorkflows.length > 1 || (approvalWorkflows?.at(0)?.approvers ?? []).length > 1) && isControlPolicy(policy); const updateApprovalMode = isAdvanceApproval ? CONST.POLICY.APPROVAL_MODE.ADVANCED : CONST.POLICY.APPROVAL_MODE.BASIC; - const displayNameForAuthorizedPayer = useMemo( - () => getDisplayNameOrDefault(getPersonalDetailByEmail(policy?.achAccount?.reimburser ?? ''), policy?.achAccount?.reimburser), - [policy?.achAccount?.reimburser], - ); + const policyReimburserEmail = policy?.achAccount?.reimburser ?? policy?.owner; + const displayNameForAuthorizedPayer = useMemo(() => getDisplayNameOrDefault(getPersonalDetailByEmail(policyReimburserEmail ?? ''), policyReimburserEmail), [policyReimburserEmail]); const isNonUSDWorkspace = policy?.outputCurrency !== CONST.CURRENCY.USD; const achData = reimbursementAccount?.achData; @@ -380,6 +378,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { const isBusinessBankAccountLocked = state === CONST.BANK_ACCOUNT.STATE.LOCKED; const shouldShowBankAccount = (!!isBankAccountFullySetup || !!bankAccountConnectedToWorkspace) && policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; + const shouldShowPayer = shouldShowBankAccount || policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL; const bankAccountPendingAction = bankAccountConnectedToWorkspace?.pendingAction; const isBankAccountPendingDelete = bankAccountPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; @@ -619,7 +618,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { let newReimbursementChoice; if (!isEnabled) { newReimbursementChoice = CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; - } else if (!!policy?.achAccount && !isCurrencySupportedForDirectReimbursement((policy?.outputCurrency ?? '') as CurrencyType)) { + } else if ((!isBankAccountFullySetup && !bankAccountConnectedToWorkspace) || !isCurrencySupportedForDirectReimbursement((policy?.outputCurrency ?? '') as CurrencyType)) { newReimbursementChoice = CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL; } else { newReimbursementChoice = CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES; @@ -744,7 +743,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { /> ) )} - {shouldShowBankAccount && policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL && ( + {shouldShowPayer && ( (policy?.achAccount?.reimburser); + const [selectedPayer, setSelectedPayer] = useState(policy?.achAccount?.reimburser ?? policy?.owner); const shouldShowSuccess = sharedBankAccountData?.shouldShowSuccess ?? false; const styles = useThemeStyles(); const {showConfirmModal} = useConfirmModal(); @@ -89,6 +89,8 @@ function WorkspaceWorkflowsPayerPage({route, policy, personalDetails, isLoadingR const ownerDetails = policy?.owner ? getPersonalDetailByEmail(policy?.owner) : undefined; const accountID = selectedPayer ? policyMemberEmailsToAccountIDs?.[selectedPayer] : ''; const authorizedPayerEmail = personalDetails?.[accountID]?.login ?? ''; + const isManualReimbursement = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL; + const isAutoReimbursement = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES; const isDeletedPolicyEmployee = (policyEmployee: PolicyEmployee) => !isOffline && policyEmployee.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyEmployee.errors); @@ -177,7 +179,11 @@ function WorkspaceWorkflowsPayerPage({route, policy, personalDetails, isLoadingR setIsAlertVisible(true); return; } - if (policy?.achAccount?.reimburser === authorizedPayerEmail || policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES) { + // When no payer is stored on the policy, the owner is the fallback payer + const reimburserEmail = policy?.achAccount?.reimburser ?? policy?.owner; + + // Skip bank-account sharing when the selection matches the payer, or when manual reimbursement has no bank account to share. + if (reimburserEmail === authorizedPayerEmail || !isAutoReimbursement) { Navigation.goBack(); return; } @@ -198,8 +204,14 @@ function WorkspaceWorkflowsPayerPage({route, policy, personalDetails, isLoadingR if (!selectedPayer) { return; } + + if (isManualReimbursement || !bankAccountID) { + onButtonPress(); + return; + } + const isSelectedPayerOwner = policy?.owner === selectedPayer; - const isSelectedAlreadyAPayer = policy?.achAccount?.reimburser === selectedPayer; + const isSelectedAlreadyAPayer = (policy?.achAccount?.reimburser ?? policy?.owner) === selectedPayer; const isAccountAlreadyShared = bankAccountInfo?.accountData?.sharees ? bankAccountInfo?.accountData.sharees.includes(selectedPayer) : false; const isAccountAlreadySharedOnMainBankAccount = policy?.achAccount?.sharees ? policy?.achAccount.sharees.includes(selectedPayer) : false; @@ -248,10 +260,7 @@ function WorkspaceWorkflowsPayerPage({route, policy, personalDetails, isLoadingR const setPolicyAuthorizedPayer = (member: MemberOption) => setSelectedPayer(personalDetails?.[member.accountID]?.login); const shouldShowBlockingPage = - (isEmptyObject(policy) && !isLoadingReportData) || - isPendingDeletePolicy(policy) || - policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO || - policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL; + (isEmptyObject(policy) && !isLoadingReportData) || isPendingDeletePolicy(policy) || policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; const totalNumberOfEmployeesEitherOwnerOrAdmin = Object.entries(policy?.employeeList ?? {}).filter(([email, policyEmployee]) => { const isOwner = policy?.owner === email; diff --git a/tests/actions/IOUTest/ReportWorkflowTest.ts b/tests/actions/IOUTest/ReportWorkflowTest.ts index b53e4e2e9d23..0838ba418299 100644 --- a/tests/actions/IOUTest/ReportWorkflowTest.ts +++ b/tests/actions/IOUTest/ReportWorkflowTest.ts @@ -2650,6 +2650,43 @@ describe('actions/IOU/ReportWorkflow', () => { expect(canIOUBePaid(fakeReport, policyChat, fakePolicy, {}, RORY_EMAIL, RORY_ACCOUNT_ID, zeroAmountNonReimbursableTransactions, false)).toBeFalsy(); expect(canIOUBePaid(fakeReport, policyChat, fakePolicy, {}, RORY_EMAIL, RORY_ACCOUNT_ID, zeroAmountNonReimbursableTransactions, true)).toBeFalsy(); }); + + it('allows non-reimburser admin to pay in manual reimbursement mode', async () => { + const policyChat = createRandomReport(1, CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT); + + const fakePolicy: Policy = { + ...createRandomPolicy(1), + id: 'AA', + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.OPTIONAL, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, + role: CONST.POLICY.ROLE.ADMIN, + achAccount: { + reimburser: CARLOS_EMAIL, + bankAccountID: 1, + accountNumber: '1234567890', + routingNumber: '987654321', + addressName: 'Test Address', + bankName: 'Test Bank', + }, + }; + + const fakeReport: Report = { + ...createRandomReport(1, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + policyID: 'AA', + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + ownerAccountID: CARLOS_ACCOUNT_ID, + managerID: RORY_ACCOUNT_ID, + isWaitingOnBankAccount: false, + total: -10000, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + + expect(canIOUBePaid(fakeReport, policyChat, fakePolicy, {}, RORY_EMAIL, RORY_ACCOUNT_ID, [], false)).toBeTruthy(); + }); }); describe('retractReport', () => { @@ -3657,6 +3694,7 @@ describe('actions/IOU/ReportWorkflow', () => { type: CONST.POLICY.TYPE.TEAM, approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, role: CONST.POLICY.ROLE.ADMIN, + owner: RORY_EMAIL, reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, }; @@ -3927,6 +3965,7 @@ describe('actions/IOU/ReportWorkflow', () => { type: CONST.POLICY.TYPE.TEAM, approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, role: CONST.POLICY.ROLE.ADMIN, + owner: RORY_EMAIL, reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, }; @@ -4367,6 +4406,7 @@ describe('actions/IOU/ReportWorkflow', () => { type: CONST.POLICY.TYPE.TEAM, approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, role: CONST.POLICY.ROLE.ADMIN, + owner: RORY_EMAIL, reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, }; @@ -4474,6 +4514,7 @@ describe('actions/IOU/ReportWorkflow', () => { type: CONST.POLICY.TYPE.TEAM, approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, role: CONST.POLICY.ROLE.ADMIN, + owner: RORY_EMAIL, reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, }; @@ -4538,6 +4579,7 @@ describe('actions/IOU/ReportWorkflow', () => { type: CONST.POLICY.TYPE.TEAM, approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, role: CONST.POLICY.ROLE.ADMIN, + owner: RORY_EMAIL, reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, }; @@ -4620,5 +4662,66 @@ describe('actions/IOU/ReportWorkflow', () => { const result = getBadgeFromIOUReport(fakeIouReport, fakeChatReport, fakePolicy, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID); expect(result).toBeUndefined(); }); + + it('should not return PAY badge for non-reimburser admin in manual mode with designated payer', async () => { + const iouReportID = '1700'; + const chatReportID = '1701'; + const policyID = '1702'; + const reimburserEmail = 'designated-payer@test.com'; + + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + id: policyID, + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + role: CONST.POLICY.ROLE.ADMIN, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, + achAccount: { + reimburser: reimburserEmail, + bankAccountID: 1, + accountNumber: '1234567890', + routingNumber: '987654321', + addressName: 'Test Address', + bankName: 'Test Bank', + }, + }; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: chatReportID, + policyID, + }; + + const fakeIouReport: Report = { + ...createRandomReport(Number(iouReportID), CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT), + reportID: iouReportID, + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.APPROVED, + managerID: RORY_ACCOUNT_ID, + total: -10000, + nonReimbursableTotal: 0, + isWaitingOnBankAccount: false, + }; + + const fakeTransaction: Transaction = { + ...createRandomTransaction(0), + reportID: iouReportID, + amount: 10000, + status: CONST.TRANSACTION.STATUS.POSTED, + bank: '', + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, fakeIouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction.transactionID}`, fakeTransaction); + await waitForBatchedUpdates(); + + // RORY_EMAIL is not the designated payer, so they should not get the PAY badge + const result = getBadgeFromIOUReport(fakeIouReport, fakeChatReport, fakePolicy, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID); + expect(result).toBeUndefined(); + }); }); }); diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index fa525a6098e3..a733e9f3ecf0 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -4,6 +4,7 @@ import Onyx from 'react-native-onyx'; import {WRITE_COMMANDS} from '@libs/API/types'; import GoogleTagManager from '@libs/GoogleTagManager'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import {isPolicyPayer} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; @@ -5544,6 +5545,51 @@ describe('actions/Policy', () => { expect(updatedPolicy?.pendingFields?.reimburser).toBeUndefined(); expect(updatedPolicy?.errorFields?.reimburser).toBeTruthy(); }); + + it('should update workspace payer in manual reimbursement mode', async () => { + // Given a workspace in manual reimbursement mode with no designated payer + const policy = { + ...createRandomPolicy(0), + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, + reimburser: '', + achAccount: {reimburser: ''}, + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + mockFetch.pause(); + + // When setting a designated payer in manual mode + Policy.setWorkspacePayer(policy.id, 'payer@manual.com', policy.reimburser); + + // Then optimistic data should set the reimburser + await waitForBatchedUpdates(); + let updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`); + expect(updatedPolicy?.reimburser).toBe('payer@manual.com'); + expect(updatedPolicy?.achAccount?.reimburser).toBe('payer@manual.com'); + expect(updatedPolicy?.reimbursementChoice).toBe(CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL); + expect(updatedPolicy?.pendingFields?.reimburser).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + + // When the fetch resumes and succeeds + await mockFetch.resume(); + + // Then pendingFields should be cleared and reimburser preserved + updatedPolicy = await getOnyxValue(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`); + expect(updatedPolicy?.achAccount?.reimburser).toBe('payer@manual.com'); + expect(updatedPolicy?.pendingFields?.reimburser).toBeUndefined(); + }); + }); + + describe('isPolicyPayer', () => { + it('returns true only for workspace owner when manual reimbursement has no achAccount reimburser', () => { + const policy = { + ...createRandomPolicy(0), + role: CONST.POLICY.ROLE.ADMIN, + owner: 'owner@test.com', + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, + }; + + expect(isPolicyPayer(policy, 'owner@test.com')).toBe(true); + expect(isPolicyPayer(policy, 'other-admin@test.com')).toBe(false); + }); }); describe('setPolicyPreventSelfApproval', () => { diff --git a/tests/actions/ReimbursementAccountTest.ts b/tests/actions/ReimbursementAccountTest.ts index eb9eb0fb2e7d..cb2cd5df222f 100644 --- a/tests/actions/ReimbursementAccountTest.ts +++ b/tests/actions/ReimbursementAccountTest.ts @@ -15,6 +15,17 @@ const bankAccountID = 1; const policyID = '1234567890'; const session = {email: TEST_EMAIL, accountID: TEST_ACCOUNT_ID}; +function expectDisconnectedAchAccount(achAccount: ACHAccount | null | undefined, reimburser: string) { + expect(achAccount?.reimburser).toBe(reimburser); + expect(achAccount?.bankAccountID).toBeFalsy(); + expect(achAccount?.accountNumber).toBeFalsy(); + expect(achAccount?.addressName).toBeFalsy(); + expect(achAccount?.bankName).toBeFalsy(); + expect(achAccount?.state).toBeFalsy(); + expect(achAccount?.routingNumber).toBeFalsy(); + expect(achAccount?.sharees).toBeFalsy(); +} + describe('ReimbursementAccount', () => { beforeAll(() => { Onyx.init({ @@ -35,7 +46,7 @@ describe('ReimbursementAccount', () => { }); it('should reset the USDBankAccount', async () => { - (fetch as MockFetch)?.pause?.(); + mockFetch.pause?.(); const achAccount: ACHAccount = { bankAccountID, addressName: 'Test Address', @@ -54,7 +65,7 @@ describe('ReimbursementAccount', () => { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, callback: (policy) => { Onyx.disconnect(connection); - expect(policy?.achAccount).toBeUndefined(); + expectDisconnectedAchAccount(policy?.achAccount, TEST_EMAIL); resolve(); }, }); @@ -63,7 +74,7 @@ describe('ReimbursementAccount', () => { }); it('should optimistically mark bank account as pending deletion', async () => { - (fetch as MockFetch)?.pause?.(); + mockFetch.pause?.(); const achAccount: ACHAccount = { bankAccountID, addressName: 'Test Address', @@ -126,7 +137,7 @@ describe('ReimbursementAccount', () => { }); it('should optimistically mark bank account as pending deletion', async () => { - (fetch as MockFetch)?.pause?.(); + mockFetch.pause?.(); const achAccount: ACHAccount = { bankAccountID, addressName: 'Test Address', @@ -180,8 +191,8 @@ describe('ReimbursementAccount', () => { }); }); - it('should clear policy achAccount optimistically', async () => { - (fetch as MockFetch)?.pause?.(); + it('should preserve designated payer and clear bank fields optimistically', async () => { + mockFetch.pause?.(); const achAccount: ACHAccount = { bankAccountID, addressName: 'Test Address', @@ -199,7 +210,7 @@ describe('ReimbursementAccount', () => { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, callback: (policy) => { Onyx.disconnect(connection); - expect(policy?.achAccount).toBeUndefined(); + expectDisconnectedAchAccount(policy?.achAccount, TEST_EMAIL); resolve(); }, }); @@ -220,11 +231,63 @@ describe('ReimbursementAccount', () => { await waitForBatchedUpdates(); return new Promise((resolve) => { - const connection = Onyx.connect({ + const reimbursementConnection = Onyx.connect({ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, callback: (reimbursementAccount) => { - Onyx.disconnect(connection); + Onyx.disconnect(reimbursementConnection); expect(reimbursementAccount).toEqual(CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA); + }, + }); + const policyConnection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + callback: (policy) => { + Onyx.disconnect(policyConnection); + expectDisconnectedAchAccount(policy?.achAccount, TEST_EMAIL); + resolve(); + }, + }); + }); + }); + + it('should preserve designated payer when it differs from owner', async () => { + const designatedPayer = 'payer@test.com'; + const policyOwner = 'owner@test.com'; + const achAccount: ACHAccount = { + bankAccountID, + addressName: 'Test Address', + bankName: 'Test Bank', + reimburser: designatedPayer, + accountNumber: '1234567890', + routingNumber: '123456789', + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {achAccount}); + resetNonUSDBankAccount(policyID, achAccount, bankAccountID, undefined, policyOwner); + + await waitForBatchedUpdates(); + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + callback: (policy) => { + Onyx.disconnect(connection); + expectDisconnectedAchAccount(policy?.achAccount, designatedPayer); + resolve(); + }, + }); + }); + }); + + it('should fall back to owner when achAccount has no reimburser', async () => { + const policyOwner = 'owner@test.com'; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {}); + resetNonUSDBankAccount(policyID, undefined, bankAccountID, undefined, policyOwner); + + await waitForBatchedUpdates(); + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + callback: (policy) => { + Onyx.disconnect(connection); + expectDisconnectedAchAccount(policy?.achAccount, policyOwner); resolve(); }, }); diff --git a/tests/actions/ReportPreviewActionUtilsTest.ts b/tests/actions/ReportPreviewActionUtilsTest.ts index 6d8460827151..fc53f7b82507 100644 --- a/tests/actions/ReportPreviewActionUtilsTest.ts +++ b/tests/actions/ReportPreviewActionUtilsTest.ts @@ -14,6 +14,7 @@ import * as InvoiceData from '../data/Invoice'; import type {InvoiceTestData} from '../data/Invoice'; import createRandomPolicy from '../utils/collections/policies'; import {createRandomReport} from '../utils/collections/reports'; +import createRandomTransaction from '../utils/collections/transaction'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; const CURRENT_USER_ACCOUNT_ID = 1; @@ -703,6 +704,54 @@ describe('getReportPreviewAction', () => { ).toBe(CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY); }); + it('canPay should return PAY for non-reimburser admin in manual reimbursement mode', async () => { + const DESIGNATED_PAYER_EMAIL = 'designated-payer@mail.com'; + const report = { + ...createRandomReport(REPORT_ID, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID + 1, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + total: -100, + isWaitingOnBankAccount: false, + }; + + const policy = createRandomPolicy(0); + policy.role = CONST.POLICY.ROLE.ADMIN; + policy.type = CONST.POLICY.TYPE.CORPORATE; + policy.reimbursementChoice = CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL; + policy.achAccount = { + reimburser: DESIGNATED_PAYER_EMAIL, + bankAccountID: 1, + accountNumber: '1234567890', + routingNumber: '987654321', + addressName: 'Test Address', + bankName: 'Test Bank', + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + const transaction = { + ...createRandomTransaction(REPORT_ID), + reportID: `${REPORT_ID}`, + }; + + const {result: isReportArchived} = renderHook(() => useReportIsArchived(report?.parentReportID)); + await waitForBatchedUpdatesWithAct(); + expect( + getReportPreviewAction({ + isReportArchived: isReportArchived.current, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + currentUserLogin: CURRENT_USER_EMAIL, + report, + policy, + transactions: [transaction], + bankAccountList: {}, + reportMetadata: undefined, + ownerLogin: CURRENT_USER_EMAIL, + }), + ).toBe(CONST.REPORT.REPORT_PREVIEW_ACTIONS.PAY); + }); + it('canPay should return false for Expense report with zero total amount', async () => { const report = { ...createRandomReport(REPORT_ID, undefined), diff --git a/tests/ui/WorkspaceWorkflowsPayerRowTest.tsx b/tests/ui/WorkspaceWorkflowsPayerRowTest.tsx index d4e3dccdb0b7..fe4a8d087452 100644 --- a/tests/ui/WorkspaceWorkflowsPayerRowTest.tsx +++ b/tests/ui/WorkspaceWorkflowsPayerRowTest.tsx @@ -137,22 +137,13 @@ describe('WorkspaceWorkflowsPage - Payer row visibility', () => { expect(screen.getByText(TestHelper.translateLocal('workflowsPayerPage.payer'))).toBeOnTheScreen(); }); - it('hides the Payer row when reimbursementChoice is REIMBURSEMENT_MANUAL', async () => { + it('shows the Payer row when reimbursementChoice is REIMBURSEMENT_MANUAL', async () => { await TestHelper.signInWithTestUser(); await act(async () => { await Onyx.merge( `${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, buildPolicy({ reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, - achAccount: { - reimburser: 'test@user.com', - bankAccountID: 123456, - accountNumber: '1234567890', - routingNumber: '011000015', - bankName: 'Test Bank', - addressName: 'Test Address', - state: CONST.BANK_ACCOUNT.STATE.OPEN, - }, }), ); }); @@ -160,7 +151,7 @@ describe('WorkspaceWorkflowsPage - Payer row visibility', () => { renderPage(); await waitForBatchedUpdatesWithAct(); - expect(screen.queryByText(TestHelper.translateLocal('workflowsPayerPage.payer'))).not.toBeOnTheScreen(); + expect(screen.getByText(TestHelper.translateLocal('workflowsPayerPage.payer'))).toBeOnTheScreen(); }); it('shows the Payer row when reimbursementChoice is undefined (legacy workspaces)', async () => { diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 7dcbbea2b779..2c2223ed9c24 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -8510,7 +8510,151 @@ describe('ReportUtils', () => { // IOU receiver (payer/manager) should still be able to pay even without access to the policy expect(isPayer(currentUserAccountID, currentUserEmail, iouReportWithPolicyID, undefined, undefined, false)).toBe(true); }); + + it('should return true for designated reimburser in manual reimbursement mode', () => { + const reimburserEmail = 'reimburser@manual-test.com'; + const reimburserAccountID = 700; + + const manualPolicyWithReimburser: Policy = { + ...policyTest, + role: CONST.POLICY.ROLE.ADMIN, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, + employeeList: { + [reimburserEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + }, + }, + achAccount: { + reimburser: reimburserEmail, + bankAccountID: 1, + accountNumber: '1234567890', + routingNumber: '987654321', + addressName: 'Test Address', + bankName: 'Test Bank', + }, + }; + + expect(isPayer(reimburserAccountID, reimburserEmail, approvedReport, undefined, manualPolicyWithReimburser, false)).toBe(true); + }); + + it('should return false for non-reimburser admin in manual reimbursement mode with designated payer', () => { + const otherAdminEmail = 'other-admin@manual-test.com'; + const otherAdminAccountID = 701; + const reimburserEmail = 'reimburser@manual-test.com'; + + const manualPolicyWithReimburser: Policy = { + ...policyTest, + role: CONST.POLICY.ROLE.ADMIN, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, + employeeList: { + [otherAdminEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + }, + [reimburserEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + }, + }, + achAccount: { + reimburser: reimburserEmail, + bankAccountID: 1, + accountNumber: '1234567890', + routingNumber: '987654321', + addressName: 'Test Address', + bankName: 'Test Bank', + }, + }; + + expect(isPayer(otherAdminAccountID, otherAdminEmail, approvedReport, undefined, manualPolicyWithReimburser, false)).toBe(false); + }); + + it('should return true for policy owner in manual reimbursement mode without explicit reimburser', () => { + const ownerEmail = 'owner@manual-fallback.com'; + const ownerAccountID = 702; + const otherAdminEmail = 'other-admin@manual-fallback.com'; + const otherAdminAccountID = 703; + + const manualPolicyNoReimburser: Policy = { + ...policyTest, + owner: ownerEmail, + role: CONST.POLICY.ROLE.ADMIN, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, + employeeList: { + [ownerEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + }, + [otherAdminEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + }, + }, + }; + + expect(isPayer(ownerAccountID, ownerEmail, approvedReport, undefined, manualPolicyNoReimburser, false)).toBe(true); + expect(isPayer(otherAdminAccountID, otherAdminEmail, approvedReport, undefined, manualPolicyNoReimburser, false)).toBe(false); + }); + + it('should return true for any admin when onlyShowPayElsewhere is true even if reimbursement is disabled and a reimburser is set', () => { + const adminEmail = 'admin@pay-elsewhere.com'; + const adminAccountID = 703; + const reimburserEmail = 'reimburser@pay-elsewhere.com'; + + const policyWithReimbursementDisabled: Policy = { + ...policyTest, + role: CONST.POLICY.ROLE.ADMIN, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO, + employeeList: { + [adminEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + }, + [reimburserEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + }, + }, + achAccount: { + reimburser: reimburserEmail, + bankAccountID: 1, + accountNumber: '1234567890', + routingNumber: '987654321', + addressName: 'Test Address', + bankName: 'Test Bank', + }, + }; + + expect(isPayer(adminAccountID, adminEmail, approvedReport, undefined, policyWithReimbursementDisabled, true)).toBe(true); + }); + + it('should return false for non-reimburser admin in manual mode with designated payer when onlyShowPayElsewhere is true', () => { + const otherAdminEmail = 'other-admin@manual-pay-elsewhere.com'; + const otherAdminAccountID = 704; + const reimburserEmail = 'reimburser@manual-pay-elsewhere.com'; + const reimburserAccountID = 705; + + const manualPolicyWithReimburser: Policy = { + ...policyTest, + role: CONST.POLICY.ROLE.ADMIN, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL, + employeeList: { + [otherAdminEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + }, + [reimburserEmail]: { + role: CONST.POLICY.ROLE.ADMIN, + }, + }, + achAccount: { + reimburser: reimburserEmail, + bankAccountID: 1, + accountNumber: '1234567890', + routingNumber: '987654321', + addressName: 'Test Address', + bankName: 'Test Bank', + }, + }; + + expect(isPayer(otherAdminAccountID, otherAdminEmail, approvedReport, undefined, manualPolicyWithReimburser, true)).toBe(false); + expect(isPayer(reimburserAccountID, reimburserEmail, approvedReport, undefined, manualPolicyWithReimburser, true)).toBe(true); + }); }); + describe('buildReportNameFromParticipantNames', () => { beforeAll(async () => { await Onyx.set(ONYXKEYS.SESSION, {email: currentUserEmail, accountID: currentUserAccountID});