From f45b6164d5bd04d5b3b590074be73667f1d76b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Mon, 12 Jan 2026 13:22:12 +0100 Subject: [PATCH 1/9] fix: New flow briefly shown --- src/libs/actions/Policy/Policy.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index fc4f9daab0c3c..2a9671de00f38 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -76,6 +76,7 @@ import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; +import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import {createFile} from '@libs/fileDownload/FileUtils'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import GoogleTagManager from '@libs/GoogleTagManager'; @@ -952,7 +953,7 @@ function setWorkspaceReimbursement({policyID, reimbursementChoice, bankAccountID const policy = getPolicy(policyID); const lastUsedPaymentMethod = typeof lastPaymentMethod === 'string' ? lastPaymentMethod : lastPaymentMethod?.expense?.name; - const optimisticData: Array> = [ + const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, @@ -965,6 +966,13 @@ function setWorkspaceReimbursement({policyID, reimbursementChoice, bankAccountID pendingFields: {reimbursementChoice: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + isLoading: true, + }, + }, ]; const successData: Array> = [ From ba8a28e4076ce3c0cbe81f6003e0dc4c00c99cbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Mon, 12 Jan 2026 13:33:53 +0100 Subject: [PATCH 2/9] fix: only connect if different --- ...ConnectExistingBusinessBankAccountPage.tsx | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index a34bc943557dd..9cfc9816d8056 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -27,6 +27,7 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness const policyID = route.params?.policyID; const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: false}); const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: true}); const policyName = policy?.name ?? ''; const policyCurrency = policy?.outputCurrency ?? ''; const {shouldUseNarrowLayout} = useResponsiveLayout(); @@ -42,15 +43,19 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness if (policyID === undefined) { return; } - const newReimburserEmail = policy?.achAccount?.reimburser ?? policy?.owner ?? ''; - setWorkspaceReimbursement({ - policyID, - reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, - bankAccountID: methodID ?? CONST.DEFAULT_NUMBER_ID, - reimburserEmail: newReimburserEmail, - lastPaymentMethod: lastPaymentMethod?.[policyID], - shouldUpdateLastPaymentMethod: accountData?.state === CONST.BANK_ACCOUNT.STATE.OPEN, - }); + + // Only connect partially setup account if it's different from the existing one + if (reimbursementAccount?.achData?.bankAccountID !== methodID) { + const newReimburserEmail = policy?.achAccount?.reimburser ?? policy?.owner ?? ''; + setWorkspaceReimbursement({ + policyID, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, + bankAccountID: methodID ?? CONST.DEFAULT_NUMBER_ID, + reimburserEmail: newReimburserEmail, + lastPaymentMethod: lastPaymentMethod?.[policyID], + shouldUpdateLastPaymentMethod: accountData?.state === CONST.BANK_ACCOUNT.STATE.OPEN, + }); + } Navigation.setNavigationActionToMicrotaskQueue(() => { if (isBankAccountPartiallySetup(accountData?.state)) { From 7c03ea2a4d9cab603353f6dca2582e71fb7bf2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Mon, 12 Jan 2026 13:45:18 +0100 Subject: [PATCH 3/9] fix: lint --- src/libs/actions/Policy/Policy.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 2a9671de00f38..8dcea15277962 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -76,7 +76,6 @@ import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; -import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import {createFile} from '@libs/fileDownload/FileUtils'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import GoogleTagManager from '@libs/GoogleTagManager'; From 73da2a2b0bd2cdd99dc7e5046c841774910e400c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Tue, 13 Jan 2026 13:21:53 +0100 Subject: [PATCH 4/9] fix: pass workspace currency to connect exisiting bank account screen --- src/components/KYCWall/BaseKYCWall.tsx | 4 ++-- src/components/KYCWall/types.ts | 4 ++-- src/components/SettlementButton/index.tsx | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index be3641aa35082..99d7bd67df751 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -53,7 +53,7 @@ function KYCWall({ source, shouldShowPersonalBankAccountOption = false, ref, - currency, + policyCurrency, }: KYCWallProps) { const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET, {canBeMissing: true}); const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS, {canBeMissing: true}); @@ -119,7 +119,7 @@ function KYCWall({ setPositionAddPaymentMenu(position); }, [getAnchorPosition]); - const canLinkExistingBusinessBankAccount = getEligibleExistingBusinessBankAccounts(bankAccountList, currency, true).length > 0; + const canLinkExistingBusinessBankAccount = getEligibleExistingBusinessBankAccounts(bankAccountList, policyCurrency, true).length > 0; const selectPaymentMethod = useCallback( (paymentMethod?: PaymentMethod, policy?: Policy) => { diff --git a/src/components/KYCWall/types.ts b/src/components/KYCWall/types.ts index 80f6be6afbc95..3a86084a89e53 100644 --- a/src/components/KYCWall/types.ts +++ b/src/components/KYCWall/types.ts @@ -78,8 +78,8 @@ type KYCWallProps = { /** Reference to the KYCWall component */ ref: ForwardedRef; - /** Currency associated with the payment */ - currency?: string; + /** Currency of the policy associated with the payment */ + policyCurrency?: string; }; type KYCWallRef = { diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 758f9b21335e1..eb956235371b1 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -98,6 +98,7 @@ function SettlementButton({ const {translate, localeCompare} = useLocalize(); const {isOffline} = useNetwork(); const policy = usePolicy(policyID); + const policyCurrency = policy?.outputCurrency; const {accountID, email} = useCurrentUserPersonalDetails(); // The app would crash due to subscribing to the entire report collection if chatReportID is an empty string. So we should have a fallback ID here. @@ -608,7 +609,7 @@ function SettlementButton({ policy={lastPaymentPolicy} anchorAlignment={kycWallAnchorAlignment} shouldShowPersonalBankAccountOption={shouldShowPersonalBankAccountOption} - currency={currency} + policyCurrency={policyCurrency} > {(triggerKYCFlow, buttonRef) => ( From 4f18a7fd0df91845ca84e304a60026a89bddf044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Tue, 13 Jan 2026 15:55:53 +0100 Subject: [PATCH 5/9] fix: rhp back button for pending acc --- src/components/KYCWall/BaseKYCWall.tsx | 7 ++++++- .../ReimbursementAccount/ReimbursementAccountPage.tsx | 2 ++ .../workspace/ConnectExistingBusinessBankAccountPage.tsx | 7 ++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index 99d7bd67df751..69b32182c8aad 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -180,11 +180,16 @@ function KYCWall({ // Setup is in progress in 2 cases: // - account already present on policy is partially setup // - account is being connected 'on the spot' while trying to pay for an expense (it won't be linked to policy yet but will appear as reimbursementAccount) - if (policy !== undefined && (isBankAccountPartiallySetup(policy?.achAccount?.state) || isBankAccountPartiallySetup(reimbursementAccount?.achData?.state))) { + if ( + policy !== undefined && + (isBankAccountPartiallySetup(policy?.achAccount?.state) || + (isBankAccountPartiallySetup(reimbursementAccount?.achData?.state) && policy?.id === reimbursementAccount?.achData?.policyID)) + ) { navigateToBankAccountRoute(policy.id); return; } + console.log(canLinkExistingBusinessBankAccount); // If user has existing bank accounts that he can connect we show the list of these accounts if (policy !== undefined && canLinkExistingBusinessBankAccount) { Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policy?.id)); diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 39bebe2feaf72..0894f2a625b84 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -415,6 +415,8 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: case CONST.BANK_ACCOUNT.STEP.VALIDATION: if ([CONST.BANK_ACCOUNT.STATE.VERIFYING, CONST.BANK_ACCOUNT.STATE.SETUP].some((value) => value === achData?.state)) { goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT); + } else if (CONST.BANK_ACCOUNT.STATE.PENDING === achData?.state) { + Navigation.closeRHPFlow(); } else { Navigation.goBack(); } diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index 9cfc9816d8056..cf62c8ad86c35 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useEffect} from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -14,6 +14,7 @@ import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation import type {ConnectExistingBankAccountNavigatorParamList} from '@navigation/types'; import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; import type {PaymentMethodPressHandlerParams} from '@pages/settings/Wallet/WalletPage/types'; +import {openReimbursementAccountPage} from '@userActions/BankAccounts'; import {setWorkspaceReimbursement} from '@userActions/Policy/Policy'; import {navigateToBankAccountRoute} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; @@ -35,6 +36,10 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness const styles = useThemeStyles(); const {translate} = useLocalize(); + useEffect(() => { + openReimbursementAccountPage('', '', '', policyID); + }, [policyID]); + const handleAddBankAccountPress = () => { navigateToBankAccountRoute(policyID); }; From 9681583a1c25a5a94b3b3e46b3cf90dea92cf7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Wed, 14 Jan 2026 14:51:34 +0100 Subject: [PATCH 6/9] fix: remove fix for one of the bugs and add tests --- src/components/KYCWall/BaseKYCWall.tsx | 7 +- .../ReimbursementAccountPage.tsx | 2 - ...ConnectExistingBusinessBankAccountPage.tsx | 4 - tests/unit/PaymentUtilsTest.ts | 74 +++++- tests/unit/WorkflowUtilsTest.ts | 250 ++++++++++++++---- 5 files changed, 269 insertions(+), 68 deletions(-) diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index 69b32182c8aad..99d7bd67df751 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -180,16 +180,11 @@ function KYCWall({ // Setup is in progress in 2 cases: // - account already present on policy is partially setup // - account is being connected 'on the spot' while trying to pay for an expense (it won't be linked to policy yet but will appear as reimbursementAccount) - if ( - policy !== undefined && - (isBankAccountPartiallySetup(policy?.achAccount?.state) || - (isBankAccountPartiallySetup(reimbursementAccount?.achData?.state) && policy?.id === reimbursementAccount?.achData?.policyID)) - ) { + if (policy !== undefined && (isBankAccountPartiallySetup(policy?.achAccount?.state) || isBankAccountPartiallySetup(reimbursementAccount?.achData?.state))) { navigateToBankAccountRoute(policy.id); return; } - console.log(canLinkExistingBusinessBankAccount); // If user has existing bank accounts that he can connect we show the list of these accounts if (policy !== undefined && canLinkExistingBusinessBankAccount) { Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policy?.id)); diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 0894f2a625b84..39bebe2feaf72 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -415,8 +415,6 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: case CONST.BANK_ACCOUNT.STEP.VALIDATION: if ([CONST.BANK_ACCOUNT.STATE.VERIFYING, CONST.BANK_ACCOUNT.STATE.SETUP].some((value) => value === achData?.state)) { goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT); - } else if (CONST.BANK_ACCOUNT.STATE.PENDING === achData?.state) { - Navigation.closeRHPFlow(); } else { Navigation.goBack(); } diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index cf62c8ad86c35..f081a145e4714 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -36,10 +36,6 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness const styles = useThemeStyles(); const {translate} = useLocalize(); - useEffect(() => { - openReimbursementAccountPage('', '', '', policyID); - }, [policyID]); - const handleAddBankAccountPress = () => { navigateToBankAccountRoute(policyID); }; diff --git a/tests/unit/PaymentUtilsTest.ts b/tests/unit/PaymentUtilsTest.ts index ebe0dfacbcd9b..cf59236bccdcc 100644 --- a/tests/unit/PaymentUtilsTest.ts +++ b/tests/unit/PaymentUtilsTest.ts @@ -1,9 +1,11 @@ import type {OnyxEntry} from 'react-native-onyx'; +import type {BankAccountMenuItem} from '@components/Search/types'; import Navigation from '@libs/Navigation/Navigation'; -import {handleUnvalidatedAccount} from '@libs/PaymentUtils'; +import {getActivePaymentType, handleUnvalidatedAccount} from '@libs/PaymentUtils'; import CONST from '@src/CONST'; import {calculateWalletTransferBalanceFee} from '@src/libs/PaymentUtils'; import type {Report} from '@src/types/onyx'; +import createRandomPolicy from '../utils/collections/policies'; jest.mock('@libs/Navigation/Navigation', () => ({ navigate: jest.fn(), @@ -58,4 +60,74 @@ describe('PaymentUtils', () => { expect(mockNavigate).toHaveBeenCalledWith(expectedRoute); }); }); + + describe('getActivePaymentType', () => { + const randomPolicyA = createRandomPolicy(1); + const randomPolicyB = createRandomPolicy(2); + const bankItem = { + text: 'Bank Account', + description: 'Test bank', + methodID: 1, + value: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, + } as BankAccountMenuItem; + + it('should return EXPENSIFY payment type when paymentMethod is PERSONAL_BANK_ACCOUNT', () => { + const result = getActivePaymentType(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, [], undefined); + + expect(result.paymentType).toBe(CONST.IOU.PAYMENT_TYPE.EXPENSIFY); + expect(result.shouldSelectPaymentMethod).toBe(true); + expect(result.selectedPolicy).toBeUndefined(); + }); + + it('should return VBBA payment type when paymentMethod is BUSINESS_BANK_ACCOUNT', () => { + const result = getActivePaymentType(CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT, [], undefined); + + expect(result.paymentType).toBe(CONST.IOU.PAYMENT_TYPE.VBBA); + expect(result.shouldSelectPaymentMethod).toBe(true); + expect(result.selectedPolicy).toBeUndefined(); + }); + + it('should return ELSEWHERE payment type when paymentMethod is DEBIT_CARD', () => { + const result = getActivePaymentType(CONST.PAYMENT_METHODS.DEBIT_CARD, [], undefined); + + expect(result.paymentType).toBe(CONST.IOU.PAYMENT_TYPE.ELSEWHERE); + expect(result.shouldSelectPaymentMethod).toBe(true); + expect(result.selectedPolicy).toBeUndefined(); + }); + + it('should return ELSEWHERE payment type when paymentMethod is undefined', () => { + const result = getActivePaymentType(undefined, [], undefined); + + expect(result.paymentType).toBe(CONST.IOU.PAYMENT_TYPE.ELSEWHERE); + expect(result.shouldSelectPaymentMethod).toBe(false); + expect(result.selectedPolicy).toBeUndefined(); + }); + + it('should set shouldSelectPaymentMethod to true when latestBankItems is not empty', () => { + const result = getActivePaymentType(undefined, [], [bankItem]); + + expect(result.paymentType).toBe(CONST.IOU.PAYMENT_TYPE.ELSEWHERE); + expect(result.shouldSelectPaymentMethod).toBe(true); + }); + + it('should find selectedPolicy by policyID', () => { + const result = getActivePaymentType(undefined, [randomPolicyA, randomPolicyB], undefined, randomPolicyA.id); + + expect(result.selectedPolicy).toEqual(randomPolicyA); + }); + + it('should find selectedPolicy by paymentMethod when it matches policy id (Pay via workspace scenario)', () => { + const result = getActivePaymentType(randomPolicyB.id, [randomPolicyA, randomPolicyB], undefined); + + expect(result.selectedPolicy).toEqual(randomPolicyB); + expect(result.paymentType).toBe(CONST.IOU.PAYMENT_TYPE.ELSEWHERE); + expect(result.shouldSelectPaymentMethod).toBe(false); + }); + + it('should return undefined selectedPolicy when no matching policy is found', () => { + const result = getActivePaymentType(undefined, [randomPolicyA], undefined, 'non-existent-policy'); + + expect(result.selectedPolicy).toBeUndefined(); + }); + }); }); diff --git a/tests/unit/WorkflowUtilsTest.ts b/tests/unit/WorkflowUtilsTest.ts index 285b93caa5315..faa33b3d96507 100644 --- a/tests/unit/WorkflowUtilsTest.ts +++ b/tests/unit/WorkflowUtilsTest.ts @@ -1,11 +1,22 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import * as WorkflowUtils from '@src/libs/WorkflowUtils'; +import CONST from '@src/CONST'; +import { + calculateApprovers, + convertApprovalWorkflowToPolicyEmployees, + convertPolicyEmployeesToApprovalWorkflows, + getApprovalLimitDescription, + getOpenConnectedToPolicyBusinessBankAccounts, + updateWorkflowDataOnApproverRemoval, +} from '@src/libs/WorkflowUtils'; +import type {Policy} from '@src/types/onyx'; import type {Approver, Member} from '@src/types/onyx/ApprovalWorkflow'; import type ApprovalWorkflow from '@src/types/onyx/ApprovalWorkflow'; +import type {BankAccountList} from '@src/types/onyx/BankAccount'; import type {PersonalDetailsList} from '@src/types/onyx/PersonalDetails'; import type {PolicyEmployeeList} from '@src/types/onyx/PolicyEmployee'; import type PolicyEmployee from '@src/types/onyx/PolicyEmployee'; -import * as TestHelper from '../utils/TestHelper'; +import createRandomPolicy from '../utils/collections/policies'; +import {buildPersonalDetails, localeCompare} from '../utils/TestHelper'; const personalDetails: PersonalDetailsList = {}; const personalDetailsByEmail: PersonalDetailsList = {}; @@ -50,7 +61,7 @@ describe('WorkflowUtils', () => { beforeAll(() => { for (let accountID = 0; accountID < 10; accountID++) { const email = `${accountID}@example.com`; - personalDetails[accountID] = TestHelper.buildPersonalDetails(email, accountID, email); + personalDetails[accountID] = buildPersonalDetails(email, accountID, email); personalDetailsByEmail[email] = personalDetails[accountID]; } }); @@ -59,7 +70,7 @@ describe('WorkflowUtils', () => { it('Should return no approvers for empty employees object', () => { const employees: PolicyEmployeeList = {}; const firstEmail = '1@example.com'; - const approvers = WorkflowUtils.calculateApprovers({employees, firstEmail, personalDetailsByEmail}); + const approvers = calculateApprovers({employees, firstEmail, personalDetailsByEmail}); expect(approvers).toEqual([]); }); @@ -76,7 +87,7 @@ describe('WorkflowUtils', () => { }, }; const firstEmail = '1@example.com'; - const approvers = WorkflowUtils.calculateApprovers({employees, firstEmail, personalDetailsByEmail}); + const approvers = calculateApprovers({employees, firstEmail, personalDetailsByEmail}); expect(approvers).toEqual([buildApprover(1)]); }); @@ -93,7 +104,7 @@ describe('WorkflowUtils', () => { }, }; const firstEmail = '1@example.com'; - const approvers = WorkflowUtils.calculateApprovers({employees, firstEmail, personalDetailsByEmail}); + const approvers = calculateApprovers({employees, firstEmail, personalDetailsByEmail}); expect(approvers).toEqual([buildApprover(1)]); }); @@ -122,21 +133,18 @@ describe('WorkflowUtils', () => { }, }; - expect(WorkflowUtils.calculateApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail})).toEqual([ + expect(calculateApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail})).toEqual([ buildApprover(1, {forwardsTo: '2@example.com'}), buildApprover(2, {forwardsTo: '3@example.com'}), buildApprover(3, {forwardsTo: '4@example.com'}), buildApprover(4), ]); - expect(WorkflowUtils.calculateApprovers({employees, firstEmail: '2@example.com', personalDetailsByEmail})).toEqual([ + expect(calculateApprovers({employees, firstEmail: '2@example.com', personalDetailsByEmail})).toEqual([ buildApprover(2, {forwardsTo: '3@example.com'}), buildApprover(3, {forwardsTo: '4@example.com'}), buildApprover(4), ]); - expect(WorkflowUtils.calculateApprovers({employees, firstEmail: '3@example.com', personalDetailsByEmail})).toEqual([ - buildApprover(3, {forwardsTo: '4@example.com'}), - buildApprover(4), - ]); + expect(calculateApprovers({employees, firstEmail: '3@example.com', personalDetailsByEmail})).toEqual([buildApprover(3, {forwardsTo: '4@example.com'}), buildApprover(4)]); }); it('Should return a list of approvers with circular references', () => { @@ -163,7 +171,7 @@ describe('WorkflowUtils', () => { }, }; - expect(WorkflowUtils.calculateApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail})).toEqual([ + expect(calculateApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail})).toEqual([ buildApprover(1, {forwardsTo: '2@example.com'}), buildApprover(2, {forwardsTo: '3@example.com'}), buildApprover(3, {forwardsTo: '4@example.com'}), @@ -171,7 +179,7 @@ describe('WorkflowUtils', () => { buildApprover(5, {forwardsTo: '1@example.com'}), buildApprover(1, {forwardsTo: '2@example.com', isCircularReference: true}), ]); - expect(WorkflowUtils.calculateApprovers({employees, firstEmail: '2@example.com', personalDetailsByEmail})).toEqual([ + expect(calculateApprovers({employees, firstEmail: '2@example.com', personalDetailsByEmail})).toEqual([ buildApprover(2, {forwardsTo: '3@example.com'}), buildApprover(3, {forwardsTo: '4@example.com'}), buildApprover(4, {forwardsTo: '5@example.com'}), @@ -189,7 +197,7 @@ describe('WorkflowUtils', () => { }, }; - expect(WorkflowUtils.calculateApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail})).toEqual([ + expect(calculateApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail})).toEqual([ buildApprover(1, {forwardsTo: '1@example.com'}), buildApprover(1, {forwardsTo: '1@example.com', isCircularReference: true}), ]); @@ -215,7 +223,7 @@ describe('WorkflowUtils', () => { }, }; - const approvers = WorkflowUtils.calculateApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail}); + const approvers = calculateApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail}); expect(approvers).toEqual([ buildApprover(1, {forwardsTo: '2@example.com', approvalLimit: 50000, overLimitForwardsTo: '3@example.com'}), @@ -233,7 +241,7 @@ describe('WorkflowUtils', () => { }, }; - const approvers = WorkflowUtils.calculateApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail}); + const approvers = calculateApprovers({employees, firstEmail: '1@example.com', personalDetailsByEmail}); expect(approvers).toEqual([buildApprover(1, {approvalLimit: null, overLimitForwardsTo: ''})]); }); @@ -257,7 +265,7 @@ describe('WorkflowUtils', () => { const defaultApprover = '1@example.com'; const policy = createMockPolicy(employees, defaultApprover); - const {approvalWorkflows} = WorkflowUtils.convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, localeCompare: TestHelper.localeCompare}); + const {approvalWorkflows} = convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, localeCompare}); expect(approvalWorkflows).toEqual([]); }); @@ -273,7 +281,7 @@ describe('WorkflowUtils', () => { const defaultApprover = '1@example.com'; const policy = createMockPolicy(employees, defaultApprover); - const {approvalWorkflows} = WorkflowUtils.convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, localeCompare: TestHelper.localeCompare}); + const {approvalWorkflows} = convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, localeCompare}); expect(approvalWorkflows).toEqual([]); }); @@ -294,7 +302,7 @@ describe('WorkflowUtils', () => { const defaultApprover = '1@example.com'; const policy = createMockPolicy(employees, defaultApprover); - const {approvalWorkflows} = WorkflowUtils.convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, localeCompare: TestHelper.localeCompare}); + const {approvalWorkflows} = convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, localeCompare}); expect(approvalWorkflows).toEqual([buildWorkflow([1, 2], [1], {isDefault: true})]); }); @@ -325,7 +333,7 @@ describe('WorkflowUtils', () => { const defaultApprover = '1@example.com'; const policy = createMockPolicy(employees, defaultApprover); - const {approvalWorkflows} = WorkflowUtils.convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, localeCompare: TestHelper.localeCompare}); + const {approvalWorkflows} = convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, localeCompare}); expect(approvalWorkflows).toEqual([buildWorkflow([2, 3], [1], {isDefault: true}), buildWorkflow([1, 4], [4])]); }); @@ -361,7 +369,7 @@ describe('WorkflowUtils', () => { const defaultApprover = '1@example.com'; const policy = createMockPolicy(employees, defaultApprover); - const {approvalWorkflows} = WorkflowUtils.convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, localeCompare: TestHelper.localeCompare}); + const {approvalWorkflows} = convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, localeCompare}); expect(approvalWorkflows).toEqual([buildWorkflow([3, 2], [1], {isDefault: true}), buildWorkflow([5], [3]), buildWorkflow([4, 1], [4])]); }); @@ -392,7 +400,7 @@ describe('WorkflowUtils', () => { const defaultApprover = '1@example.com'; const policy = createMockPolicy(employees, defaultApprover); - const {approvalWorkflows} = WorkflowUtils.convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, localeCompare: TestHelper.localeCompare}); + const {approvalWorkflows} = convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, localeCompare}); const defaultWorkflow = buildWorkflow([2, 3, 4], [1, 3, 4], {isDefault: true}); let firstApprover = defaultWorkflow.approvers.at(0); @@ -448,7 +456,7 @@ describe('WorkflowUtils', () => { const defaultApprover = '1@example.com'; const policy = createMockPolicy(employees, defaultApprover); - const {approvalWorkflows} = WorkflowUtils.convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, localeCompare: TestHelper.localeCompare}); + const {approvalWorkflows} = convertPolicyEmployeesToApprovalWorkflows({policy, personalDetails, localeCompare}); const defaultWorkflow = buildWorkflow([1, 4, 5, 6], [1], {isDefault: true}); const secondWorkflow = buildWorkflow([2, 3], [4, 5, 6]); @@ -471,7 +479,7 @@ describe('WorkflowUtils', () => { isDefault: true, }; - const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({previousEmployeeList: {}, approvalWorkflow, type: 'create'}); + const convertedEmployees = convertApprovalWorkflowToPolicyEmployees({previousEmployeeList: {}, approvalWorkflow, type: 'create'}); expect(convertedEmployees).toEqual({ '1@example.com': buildPolicyEmployee(1, {forwardsTo: '', overLimitForwardsTo: '', submitsTo: '1@example.com', pendingFields: {submitsTo: 'add'}}), @@ -486,7 +494,7 @@ describe('WorkflowUtils', () => { isDefault: false, }; - const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({previousEmployeeList: {}, approvalWorkflow, type: 'create'}); + const convertedEmployees = convertApprovalWorkflowToPolicyEmployees({previousEmployeeList: {}, approvalWorkflow, type: 'create'}); expect(convertedEmployees).toEqual({ '1@example.com': buildPolicyEmployee(1, {forwardsTo: '2@example.com', overLimitForwardsTo: '', pendingFields: {forwardsTo: 'add', overLimitForwardsTo: 'add'}}), @@ -505,7 +513,7 @@ describe('WorkflowUtils', () => { isDefault: false, }; - const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({previousEmployeeList: {}, approvalWorkflow, type: 'remove'}); + const convertedEmployees = convertApprovalWorkflowToPolicyEmployees({previousEmployeeList: {}, approvalWorkflow, type: 'remove'}); expect(convertedEmployees).toEqual({ '1@example.com': buildPolicyEmployee(1, { @@ -542,7 +550,7 @@ describe('WorkflowUtils', () => { isDefault: false, }; - const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({previousEmployeeList: {}, approvalWorkflow, type: 'create'}); + const convertedEmployees = convertApprovalWorkflowToPolicyEmployees({previousEmployeeList: {}, approvalWorkflow, type: 'create'}); expect(convertedEmployees).toEqual({ '1@example.com': buildPolicyEmployee(1, { @@ -570,7 +578,7 @@ describe('WorkflowUtils', () => { isDefault: false, }; - const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({previousEmployeeList, approvalWorkflow, type: 'remove'}); + const convertedEmployees = convertApprovalWorkflowToPolicyEmployees({previousEmployeeList, approvalWorkflow, type: 'remove'}); // approvalLimit should be null (not undefined) so it gets sent to the API and clears the field expect(convertedEmployees['1@example.com']?.approvalLimit).toBeNull(); @@ -592,7 +600,7 @@ describe('WorkflowUtils', () => { isDefault: false, }; - const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({previousEmployeeList, approvalWorkflow, type: 'update'}); + const convertedEmployees = convertApprovalWorkflowToPolicyEmployees({previousEmployeeList, approvalWorkflow, type: 'update'}); // pendingFields should include the fields that changed (forwardsTo didn't change since it's '' -> '') expect(convertedEmployees['1@example.com']?.pendingFields).toEqual({ @@ -618,7 +626,7 @@ describe('WorkflowUtils', () => { isDefault: false, }; - const convertedEmployees = WorkflowUtils.convertApprovalWorkflowToPolicyEmployees({previousEmployeeList, approvalWorkflow, type: 'update'}); + const convertedEmployees = convertApprovalWorkflowToPolicyEmployees({previousEmployeeList, approvalWorkflow, type: 'update'}); // Only overLimitForwardsTo changed, so only that should be in pendingFields expect(convertedEmployees['1@example.com']?.pendingFields).toEqual({ @@ -647,13 +655,13 @@ describe('WorkflowUtils', () => { return; } - const updateWorkflowDataOnApproverRemoval = WorkflowUtils.updateWorkflowDataOnApproverRemoval({ + const updateWorkflowDataOnApproverRemovalResult = updateWorkflowDataOnApproverRemoval({ approvalWorkflows: [approvalWorkflow1, approvalWorkflow2], removedApprover, ownerDetails, }); - expect(updateWorkflowDataOnApproverRemoval).toEqual([approvalWorkflow1, {...approvalWorkflow2, removeApprovalWorkflow: true}]); + expect(updateWorkflowDataOnApproverRemovalResult).toEqual([approvalWorkflow1, {...approvalWorkflow2, removeApprovalWorkflow: true}]); }); it('Should replace the approvers in Workflow 2 with the Workspace Owner if it has no approvers and the approver in Workspace (default) is different from the Workspace Owner', () => { const approvalWorkflow1: ApprovalWorkflow = { @@ -674,13 +682,13 @@ describe('WorkflowUtils', () => { return; } - const updateWorkflowDataOnApproverRemoval = WorkflowUtils.updateWorkflowDataOnApproverRemoval({ + const updateWorkflowDataOnApproverRemovalResult = updateWorkflowDataOnApproverRemoval({ approvalWorkflows: [approvalWorkflow1, approvalWorkflow2], removedApprover, ownerDetails, }); - expect(updateWorkflowDataOnApproverRemoval).toEqual([approvalWorkflow1, {...approvalWorkflow2, approvers: [buildApprover(1)]}]); + expect(updateWorkflowDataOnApproverRemovalResult).toEqual([approvalWorkflow1, {...approvalWorkflow2, approvers: [buildApprover(1)]}]); }); it('Should remove Workflow 2 if its approver is the Workspace Owner and the default Workspace approver is removed.', () => { const approvalWorkflow1: ApprovalWorkflow = { @@ -701,13 +709,13 @@ describe('WorkflowUtils', () => { return; } - const updateWorkflowDataOnApproverRemoval = WorkflowUtils.updateWorkflowDataOnApproverRemoval({ + const updateWorkflowDataOnApproverRemovalResult = updateWorkflowDataOnApproverRemoval({ approvalWorkflows: [approvalWorkflow1, approvalWorkflow2], removedApprover, ownerDetails, }); - expect(updateWorkflowDataOnApproverRemoval).toEqual([ + expect(updateWorkflowDataOnApproverRemovalResult).toEqual([ {...approvalWorkflow1, approvers: [buildApprover(1)]}, {...approvalWorkflow2, removeApprovalWorkflow: true}, ]); @@ -731,13 +739,13 @@ describe('WorkflowUtils', () => { return; } - const updateWorkflowDataOnApproverRemoval = WorkflowUtils.updateWorkflowDataOnApproverRemoval({ + const updateWorkflowDataOnApproverRemovalResult = updateWorkflowDataOnApproverRemoval({ approvalWorkflows: [approvalWorkflow1, approvalWorkflow2], removedApprover, ownerDetails, }); - expect(updateWorkflowDataOnApproverRemoval).toEqual([approvalWorkflow1, {...approvalWorkflow2, approvers: [buildApprover(2), buildApprover(3), buildApprover(1)]}]); + expect(updateWorkflowDataOnApproverRemovalResult).toEqual([approvalWorkflow1, {...approvalWorkflow2, approvers: [buildApprover(2), buildApprover(3), buildApprover(1)]}]); }); it('Should remove the approvers that have submitsTo set to the removed approver, update the removed approver to the Workspace Owner, and ensure there was a previous approver before this one', () => { const approvalWorkflow1: ApprovalWorkflow = { @@ -758,13 +766,13 @@ describe('WorkflowUtils', () => { return; } - const updateWorkflowDataOnApproverRemoval = WorkflowUtils.updateWorkflowDataOnApproverRemoval({ + const updateWorkflowDataOnApproverRemovalResult = updateWorkflowDataOnApproverRemoval({ approvalWorkflows: [approvalWorkflow1, approvalWorkflow2], removedApprover, ownerDetails, }); - expect(updateWorkflowDataOnApproverRemoval).toEqual([approvalWorkflow1, {...approvalWorkflow2, approvers: [buildApprover(2), buildApprover(1)]}]); + expect(updateWorkflowDataOnApproverRemovalResult).toEqual([approvalWorkflow1, {...approvalWorkflow2, approvers: [buildApprover(2), buildApprover(1)]}]); }); it('Should remove Workflow 2 if it has no approvers and the default Workspace approver is the approve', () => { const approvalWorkflow1: ApprovalWorkflow = { @@ -785,13 +793,13 @@ describe('WorkflowUtils', () => { return; } - const updateWorkflowDataOnApproverRemoval = WorkflowUtils.updateWorkflowDataOnApproverRemoval({ + const updateWorkflowDataOnApproverRemovalResult = updateWorkflowDataOnApproverRemoval({ approvalWorkflows: [approvalWorkflow1, approvalWorkflow2], removedApprover, ownerDetails, }); - expect(updateWorkflowDataOnApproverRemoval).toEqual([approvalWorkflow1, {...approvalWorkflow2, removeApprovalWorkflow: true}]); + expect(updateWorkflowDataOnApproverRemovalResult).toEqual([approvalWorkflow1, {...approvalWorkflow2, removeApprovalWorkflow: true}]); }); }); @@ -808,10 +816,10 @@ describe('WorkflowUtils', () => { }); it('Should return undefined when approver is undefined', () => { - const result = WorkflowUtils.getApprovalLimitDescription({ + const result = getApprovalLimitDescription({ approver: undefined, currency: 'USD', - translate: mockTranslate as unknown as Parameters[0]['translate'], + translate: mockTranslate as unknown as Parameters[0]['translate'], personalDetailsByEmail: {}, }); @@ -821,10 +829,10 @@ describe('WorkflowUtils', () => { it('Should return undefined when approvalLimit is null', () => { const approver = buildApprover(1, {approvalLimit: null, overLimitForwardsTo: '2@example.com'}); - const result = WorkflowUtils.getApprovalLimitDescription({ + const result = getApprovalLimitDescription({ approver, currency: 'USD', - translate: mockTranslate as unknown as Parameters[0]['translate'], + translate: mockTranslate as unknown as Parameters[0]['translate'], personalDetailsByEmail: {}, }); @@ -834,10 +842,10 @@ describe('WorkflowUtils', () => { it('Should return undefined when approvalLimit is undefined', () => { const approver = buildApprover(1, {approvalLimit: undefined, overLimitForwardsTo: '2@example.com'}); - const result = WorkflowUtils.getApprovalLimitDescription({ + const result = getApprovalLimitDescription({ approver, currency: 'USD', - translate: mockTranslate as unknown as Parameters[0]['translate'], + translate: mockTranslate as unknown as Parameters[0]['translate'], personalDetailsByEmail: {}, }); @@ -847,10 +855,10 @@ describe('WorkflowUtils', () => { it('Should return undefined when overLimitForwardsTo is missing', () => { const approver = buildApprover(1, {approvalLimit: 50000, overLimitForwardsTo: undefined}); - const result = WorkflowUtils.getApprovalLimitDescription({ + const result = getApprovalLimitDescription({ approver, currency: 'USD', - translate: mockTranslate as unknown as Parameters[0]['translate'], + translate: mockTranslate as unknown as Parameters[0]['translate'], personalDetailsByEmail: {}, }); @@ -860,10 +868,10 @@ describe('WorkflowUtils', () => { it('Should return description when approvalLimit and overLimitForwardsTo are set', () => { const approver = buildApprover(1, {approvalLimit: 50000, overLimitForwardsTo: '2@example.com'}); - const result = WorkflowUtils.getApprovalLimitDescription({ + const result = getApprovalLimitDescription({ approver, currency: 'USD', - translate: mockTranslate as unknown as Parameters[0]['translate'], + translate: mockTranslate as unknown as Parameters[0]['translate'], personalDetailsByEmail: {}, }); @@ -876,14 +884,146 @@ describe('WorkflowUtils', () => { '2@example.com': {accountID: 2, displayName: 'John Doe'}, }; - const result = WorkflowUtils.getApprovalLimitDescription({ + const result = getApprovalLimitDescription({ approver, currency: 'USD', - translate: mockTranslate as unknown as Parameters[0]['translate'], + translate: mockTranslate as unknown as Parameters[0]['translate'], personalDetailsByEmail: personalDetailsWithEmail, }); expect(result).toBe('Reports above $1,000.00 forward to John Doe'); }); }); + + describe('getOpenConnectedToPolicyBusinessBankAccounts', () => { + const matchingBankAccountID = 12345; + + const policyWithACH = { + ...createRandomPolicy(1), + outputCurrency: 'USD', + achAccount: { + bankAccountID: matchingBankAccountID, + }, + } as Policy; + + const openBusinessBankAccount = { + bankCurrency: 'USD', + bankCountry: 'US', + accountData: { + state: CONST.BANK_ACCOUNT.STATE.OPEN, + type: CONST.BANK_ACCOUNT.TYPE.BUSINESS, + bankAccountID: matchingBankAccountID, + }, + }; + + it('should return empty array when bankAccountList is undefined', () => { + const result = getOpenConnectedToPolicyBusinessBankAccounts(undefined, policyWithACH); + + expect(result).toEqual([]); + }); + + it('should return empty array when policy is undefined', () => { + const bankAccountList: BankAccountList = { + '1': openBusinessBankAccount, + }; + + const result = getOpenConnectedToPolicyBusinessBankAccounts(bankAccountList, undefined); + + expect(result).toEqual([]); + }); + + it('should return matching bank accounts that meet all criteria', () => { + const bankAccountList: BankAccountList = { + '1': openBusinessBankAccount, + }; + + const result = getOpenConnectedToPolicyBusinessBankAccounts(bankAccountList, policyWithACH); + + expect(result).toEqual([openBusinessBankAccount]); + }); + + it('should filter out accounts with non-matching currency', () => { + const nonMatchingCurrencyAccount = { + ...openBusinessBankAccount, + bankCurrency: 'EUR', + }; + const bankAccountList: BankAccountList = { + '1': nonMatchingCurrencyAccount, + }; + + const result = getOpenConnectedToPolicyBusinessBankAccounts(bankAccountList, policyWithACH); + + expect(result).toEqual([]); + }); + + it('should filter out accounts that are not in OPEN state', () => { + const pendingAccount = { + ...openBusinessBankAccount, + accountData: { + ...openBusinessBankAccount.accountData, + state: CONST.BANK_ACCOUNT.STATE.PENDING, + }, + }; + const bankAccountList: BankAccountList = { + '1': pendingAccount, + }; + + const result = getOpenConnectedToPolicyBusinessBankAccounts(bankAccountList, policyWithACH); + + expect(result).toEqual([]); + }); + + it('should filter out accounts that are not BUSINESS type', () => { + const personalAccount = { + ...openBusinessBankAccount, + accountData: { + ...openBusinessBankAccount.accountData, + type: CONST.BANK_ACCOUNT.TYPE.PERSONAL, + }, + }; + const bankAccountList: BankAccountList = { + '1': personalAccount, + }; + + const result = getOpenConnectedToPolicyBusinessBankAccounts(bankAccountList, policyWithACH); + + expect(result).toEqual([]); + }); + + it('should filter out accounts not linked to policy ACH account', () => { + const unlinkedAccount = { + ...openBusinessBankAccount, + accountData: { + ...openBusinessBankAccount.accountData, + bankAccountID: 99999, + }, + }; + const bankAccountList: BankAccountList = { + '1': unlinkedAccount, + }; + + const result = getOpenConnectedToPolicyBusinessBankAccounts(bankAccountList, policyWithACH); + + expect(result).toEqual([]); + }); + + it('should return multiple matching accounts', () => { + const secondMatchingAccount = { + ...openBusinessBankAccount, + accountData: { + ...openBusinessBankAccount.accountData, + bankAccountID: matchingBankAccountID, + accountNumber: '9999', + }, + }; + const bankAccountList: BankAccountList = { + '1': openBusinessBankAccount, + '2': secondMatchingAccount, + }; + + const result = getOpenConnectedToPolicyBusinessBankAccounts(bankAccountList, policyWithACH); + + expect(result).toEqual([openBusinessBankAccount, secondMatchingAccount]); + }); + }); }); From 91392408bc194e260abf89848fc12de38ce7cf93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Wed, 14 Jan 2026 16:39:51 +0100 Subject: [PATCH 7/9] fix: bulk pay --- src/components/KYCWall/BaseKYCWall.tsx | 13 ++++++------- src/components/KYCWall/types.ts | 3 --- src/components/SettlementButton/index.tsx | 2 -- src/libs/actions/Search.ts | 2 +- .../ConnectExistingBusinessBankAccountPage.tsx | 3 +-- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index 99d7bd67df751..cd453bafd69b0 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -53,7 +53,6 @@ function KYCWall({ source, shouldShowPersonalBankAccountOption = false, ref, - policyCurrency, }: KYCWallProps) { const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET, {canBeMissing: true}); const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS, {canBeMissing: true}); @@ -119,10 +118,10 @@ function KYCWall({ setPositionAddPaymentMenu(position); }, [getAnchorPosition]); - const canLinkExistingBusinessBankAccount = getEligibleExistingBusinessBankAccounts(bankAccountList, policyCurrency, true).length > 0; - const selectPaymentMethod = useCallback( (paymentMethod?: PaymentMethod, policy?: Policy) => { + const canLinkExistingBusinessBankAccount = getEligibleExistingBusinessBankAccounts(bankAccountList, policy?.outputCurrency, true).length > 0; + if (paymentMethod) { onSelectPaymentMethod(paymentMethod); } @@ -196,20 +195,20 @@ function KYCWall({ } }, [ + bankAccountList, onSelectPaymentMethod, iouReport, addDebitCardRoute, reimbursementAccount?.achData?.state, - canLinkExistingBusinessBankAccount, addBankAccountRoute, chatReport, policies, - introSelected, - formatPhoneNumber, - lastPaymentMethod, reportPreviewAction, currentUserEmail, employeeEmail, + introSelected, + formatPhoneNumber, + lastPaymentMethod, ], ); diff --git a/src/components/KYCWall/types.ts b/src/components/KYCWall/types.ts index 3a86084a89e53..59f3485442d66 100644 --- a/src/components/KYCWall/types.ts +++ b/src/components/KYCWall/types.ts @@ -77,9 +77,6 @@ type KYCWallProps = { /** Reference to the KYCWall component */ ref: ForwardedRef; - - /** Currency of the policy associated with the payment */ - policyCurrency?: string; }; type KYCWallRef = { diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index eb956235371b1..ec1bb1db0781d 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -98,7 +98,6 @@ function SettlementButton({ const {translate, localeCompare} = useLocalize(); const {isOffline} = useNetwork(); const policy = usePolicy(policyID); - const policyCurrency = policy?.outputCurrency; const {accountID, email} = useCurrentUserPersonalDetails(); // The app would crash due to subscribing to the entire report collection if chatReportID is an empty string. So we should have a fallback ID here. @@ -609,7 +608,6 @@ function SettlementButton({ policy={lastPaymentPolicy} anchorAlignment={kycWallAnchorAlignment} shouldShowPersonalBankAccountOption={shouldShowPersonalBankAccountOption} - policyCurrency={policyCurrency} > {(triggerKYCFlow, buttonRef) => ( diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 29b64221bbb50..d20634738d4a7 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -1124,7 +1124,7 @@ function handleBulkPayItemSelected(params: { showDelegateNoAccessModal, confirmPayment, } = params; - const {paymentType, selectedPolicy, shouldSelectPaymentMethod} = getActivePaymentType(item.key, activeAdminPolicies, latestBankItems); + const {paymentType, selectedPolicy, shouldSelectPaymentMethod} = getActivePaymentType(item.key, activeAdminPolicies, latestBankItems, policy?.id); // Policy id is also a last payment method so we shouldn't early return here for that case. if (!isValidBulkPayOption(item) && !selectedPolicy) { return; diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index f081a145e4714..9cfc9816d8056 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -14,7 +14,6 @@ import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation import type {ConnectExistingBankAccountNavigatorParamList} from '@navigation/types'; import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; import type {PaymentMethodPressHandlerParams} from '@pages/settings/Wallet/WalletPage/types'; -import {openReimbursementAccountPage} from '@userActions/BankAccounts'; import {setWorkspaceReimbursement} from '@userActions/Policy/Policy'; import {navigateToBankAccountRoute} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; From a767599b0536f82925d7063ab1702bc69beed1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Wed, 14 Jan 2026 16:57:16 +0100 Subject: [PATCH 8/9] fix: revert unstable flickering solution --- src/libs/actions/Policy/Policy.ts | 9 +------- .../ReimbursementAccountPage.tsx | 2 ++ ...ConnectExistingBusinessBankAccountPage.tsx | 22 ++++++++----------- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 502fb45fb9861..f532092fba19a 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -952,7 +952,7 @@ function setWorkspaceReimbursement({policyID, reimbursementChoice, bankAccountID const policy = getPolicy(policyID); const lastUsedPaymentMethod = typeof lastPaymentMethod === 'string' ? lastPaymentMethod : lastPaymentMethod?.expense?.name; - const optimisticData: Array> = [ + const optimisticData: Array = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, @@ -965,13 +965,6 @@ function setWorkspaceReimbursement({policyID, reimbursementChoice, bankAccountID pendingFields: {reimbursementChoice: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: { - isLoading: true, - }, - }, ]; const successData: Array> = [ diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 39bebe2feaf72..0894f2a625b84 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -415,6 +415,8 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: case CONST.BANK_ACCOUNT.STEP.VALIDATION: if ([CONST.BANK_ACCOUNT.STATE.VERIFYING, CONST.BANK_ACCOUNT.STATE.SETUP].some((value) => value === achData?.state)) { goToWithdrawalAccountSetupStep(CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT); + } else if (CONST.BANK_ACCOUNT.STATE.PENDING === achData?.state) { + Navigation.closeRHPFlow(); } else { Navigation.goBack(); } diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index 9cfc9816d8056..6979860029306 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -27,7 +27,6 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness const policyID = route.params?.policyID; const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: false}); const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); - const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: true}); const policyName = policy?.name ?? ''; const policyCurrency = policy?.outputCurrency ?? ''; const {shouldUseNarrowLayout} = useResponsiveLayout(); @@ -44,18 +43,15 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness return; } - // Only connect partially setup account if it's different from the existing one - if (reimbursementAccount?.achData?.bankAccountID !== methodID) { - const newReimburserEmail = policy?.achAccount?.reimburser ?? policy?.owner ?? ''; - setWorkspaceReimbursement({ - policyID, - reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, - bankAccountID: methodID ?? CONST.DEFAULT_NUMBER_ID, - reimburserEmail: newReimburserEmail, - lastPaymentMethod: lastPaymentMethod?.[policyID], - shouldUpdateLastPaymentMethod: accountData?.state === CONST.BANK_ACCOUNT.STATE.OPEN, - }); - } + const newReimburserEmail = policy?.achAccount?.reimburser ?? policy?.owner ?? ''; + setWorkspaceReimbursement({ + policyID, + reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, + bankAccountID: methodID ?? CONST.DEFAULT_NUMBER_ID, + reimburserEmail: newReimburserEmail, + lastPaymentMethod: lastPaymentMethod?.[policyID], + shouldUpdateLastPaymentMethod: accountData?.state === CONST.BANK_ACCOUNT.STATE.OPEN, + }); Navigation.setNavigationActionToMicrotaskQueue(() => { if (isBankAccountPartiallySetup(accountData?.state)) { From 2d89645189146ee7b806abfeb37a5e97dd7791b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Wed, 14 Jan 2026 16:59:08 +0100 Subject: [PATCH 9/9] fix: ts --- src/libs/actions/Policy/Policy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index f532092fba19a..ee6fa78779e73 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -952,7 +952,7 @@ function setWorkspaceReimbursement({policyID, reimbursementChoice, bankAccountID const policy = getPolicy(policyID); const lastUsedPaymentMethod = typeof lastPaymentMethod === 'string' ? lastPaymentMethod : lastPaymentMethod?.expense?.name; - const optimisticData: Array = [ + const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,