From cf47ce218f8b98708670815272fe28f314852ca9 Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Mon, 16 Mar 2026 18:05:38 +0530 Subject: [PATCH 1/2] Move Chase Plaid fallback to frontend manual flow. Skip the ConnectBankAccountWithPlaid API call for new Chase accounts, set reimbursement account step/substep in Onyx, and clear only account/routing draft fields with action tests. --- src/libs/actions/BankAccounts.ts | 16 +++++ tests/actions/BankAccountsTest.ts | 112 ++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 tests/actions/BankAccountsTest.ts diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 6ac536e761e03..12314e0f7b8dd 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -254,6 +254,22 @@ function getOnyxDataForConnectingVBBAAndLastPaymentMethod( * Submit Bank Account step with Plaid data so php can perform some checks. */ function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAccount: PlaidBankAccount, policyID: string) { + const isChaseBank = selectedPlaidBankAccount.bankName?.toLowerCase() === CONST.BANK_NAMES.CHASE; + if (bankAccountID === CONST.DEFAULT_NUMBER_ID && isChaseBank) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, { + achData: { + currentStep: CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT, + subStep: CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL, + }, + errors: null, + }); + Onyx.merge(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, { + accountNumber: '', + routingNumber: '', + }); + return; + } + const parameters: ConnectBankAccountParams = { bankAccountID, routingNumber: selectedPlaidBankAccount.routingNumber, diff --git a/tests/actions/BankAccountsTest.ts b/tests/actions/BankAccountsTest.ts new file mode 100644 index 0000000000000..0dd4688cee5b0 --- /dev/null +++ b/tests/actions/BankAccountsTest.ts @@ -0,0 +1,112 @@ +import Onyx from 'react-native-onyx'; +import {connectBankAccountWithPlaid} from '@libs/actions/BankAccounts'; +import {WRITE_COMMANDS} from '@libs/API/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReimbursementAccountForm} from '@src/types/form/ReimbursementAccountForm'; +import type PlaidBankAccount from '@src/types/onyx/PlaidBankAccount'; +import getOnyxValue from '../utils/getOnyxValue'; +import type {MockFetch} from '../utils/TestHelper'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +const POLICY_ID = 'policyID123'; + +function getPlaidBankAccount(bankName: string): PlaidBankAccount { + return { + accountNumber: '111122223333', + routingNumber: '123456789', + bankName, + plaidAccountID: 'plaidAccountID123', + plaidAccessToken: 'plaidAccessToken123', + mask: '3333', + isSavings: false, + }; +} + +describe('actions/BankAccounts', () => { + let mockFetch: MockFetch; + + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + }); + }); + + beforeEach(() => { + global.fetch = TestHelper.getGlobalFetchMock(); + mockFetch = fetch as MockFetch; + return Onyx.clear().then(waitForBatchedUpdates); + }); + + afterEach(() => { + mockFetch?.resume?.(); + }); + + describe('connectBankAccountWithPlaid', () => { + test('switches to manual flow and clears account and routing numbers for new Chase accounts', async () => { + // Given a new reimbursement account in Plaid setup with existing draft values + await Onyx.set(ONYXKEYS.REIMBURSEMENT_ACCOUNT, { + achData: { + currentStep: CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT, + subStep: CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID, + }, + }); + await Onyx.set(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, { + accountNumber: '111122223333', + routingNumber: '123456789', + plaidAccountID: 'plaidAccountID123', + plaidAccessToken: 'plaidAccessToken123', + mask: '3333', + } as Partial); + + // When we connect with Plaid for Chase on a new account + connectBankAccountWithPlaid(CONST.DEFAULT_NUMBER_ID, getPlaidBankAccount(CONST.BANK_NAMES_USER_FRIENDLY[CONST.BANK_NAMES.CHASE]), POLICY_ID); + await waitForBatchedUpdates(); + + // Then we should not call the backend, and should move user to manual with cleared account + routing numbers only + const reimbursementAccount = await getOnyxValue(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + const reimbursementAccountDraft = await getOnyxValue(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID, 0); + expect(reimbursementAccount?.achData?.currentStep).toBe(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT); + expect(reimbursementAccount?.achData?.subStep).toBe(CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL); + expect(reimbursementAccount?.errors ?? null).toBeNull(); + expect(reimbursementAccountDraft?.accountNumber).toBe(''); + expect(reimbursementAccountDraft?.routingNumber).toBe(''); + expect(reimbursementAccountDraft?.plaidAccountID).toBe('plaidAccountID123'); + expect(reimbursementAccountDraft?.plaidAccessToken).toBe('plaidAccessToken123'); + expect(reimbursementAccountDraft?.mask).toBe('3333'); + }); + + test('keeps API flow for existing Chase accounts', () => { + // Given an existing Chase bank account + const bankAccountID = 123; + const selectedPlaidBankAccount = getPlaidBankAccount(CONST.BANK_NAMES_USER_FRIENDLY[CONST.BANK_NAMES.CHASE]); + + // When we connect with Plaid + connectBankAccountWithPlaid(bankAccountID, selectedPlaidBankAccount, POLICY_ID); + return waitForBatchedUpdates().then(() => { + // Then we should call the existing API command + TestHelper.expectAPICommandToHaveBeenCalled(WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID, 1); + const call = TestHelper.getFetchMockCalls(WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID).at(0); + const body = (call?.at(1) as RequestInit)?.body; + const params = body instanceof FormData ? Object.fromEntries(body) : {}; + + expect(params).toEqual( + expect.objectContaining({ + bankAccountID: `${bankAccountID}`, + routingNumber: selectedPlaidBankAccount.routingNumber, + accountNumber: selectedPlaidBankAccount.accountNumber, + bank: selectedPlaidBankAccount.bankName, + plaidAccountID: selectedPlaidBankAccount.plaidAccountID, + plaidAccessToken: selectedPlaidBankAccount.plaidAccessToken, + plaidMask: selectedPlaidBankAccount.mask, + isSavings: `${selectedPlaidBankAccount.isSavings}`, + policyID: POLICY_ID, + }), + ); + }); + }); + }); +}); From 765a68b9e8ebdcd6ce0e22d31d7cf066aefd0c35 Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya Date: Thu, 19 Mar 2026 11:19:10 +0530 Subject: [PATCH 2/2] Clarify Chase Plaid fallback test names. Rename Chase-specific connectBankAccountWithPlaid tests and update expectation wording so they clearly distinguish new-account short-circuit behavior from non-zero bankAccountID API flow. --- tests/actions/BankAccountsTest.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/actions/BankAccountsTest.ts b/tests/actions/BankAccountsTest.ts index 0dd4688cee5b0..612dd79480093 100644 --- a/tests/actions/BankAccountsTest.ts +++ b/tests/actions/BankAccountsTest.ts @@ -44,7 +44,7 @@ describe('actions/BankAccounts', () => { }); describe('connectBankAccountWithPlaid', () => { - test('switches to manual flow and clears account and routing numbers for new Chase accounts', async () => { + test('short-circuits new Chase accounts to manual flow and clears account/routing draft fields', async () => { // Given a new reimbursement account in Plaid setup with existing draft values await Onyx.set(ONYXKEYS.REIMBURSEMENT_ACCOUNT, { achData: { @@ -64,7 +64,7 @@ describe('actions/BankAccounts', () => { connectBankAccountWithPlaid(CONST.DEFAULT_NUMBER_ID, getPlaidBankAccount(CONST.BANK_NAMES_USER_FRIENDLY[CONST.BANK_NAMES.CHASE]), POLICY_ID); await waitForBatchedUpdates(); - // Then we should not call the backend, and should move user to manual with cleared account + routing numbers only + // Then we should not call the backend, and should move user to manual with cleared account/routing draft fields const reimbursementAccount = await getOnyxValue(ONYXKEYS.REIMBURSEMENT_ACCOUNT); const reimbursementAccountDraft = await getOnyxValue(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); @@ -79,7 +79,7 @@ describe('actions/BankAccounts', () => { expect(reimbursementAccountDraft?.mask).toBe('3333'); }); - test('keeps API flow for existing Chase accounts', () => { + test('does not short-circuit Chase flow when bankAccountID is non-zero', () => { // Given an existing Chase bank account const bankAccountID = 123; const selectedPlaidBankAccount = getPlaidBankAccount(CONST.BANK_NAMES_USER_FRIENDLY[CONST.BANK_NAMES.CHASE]);