Commit ca3bad601c5 for woocommerce

commit ca3bad601c5e25530ac4c4090d3d11a30f2038c2
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date:   Fri Aug 7 21:56:06 2026 +0300

    [Payments NOX] Fix the default country for new BACS bank accounts (#67478)

    * Start new bank accounts in the merchant's business location

    Adding a bank account on the Direct bank transfer page opened the country
    field on the store's base country. The Payments settings header lets the
    merchant state a business location separately from the store address, and
    that is the more direct answer to where they bank — it is also the country
    the rest of the Payments screens already reason about.

    Prefer it, keeping the base country as the fallback. That fallback now
    drops the state suffix the option carries ('US:CA'): passed through whole
    it matched no entry in the country field and no case in the rules that
    pick the routing-number label, so a store with a state in its address got
    neither a selected country nor the right routing field.

    * Await the userEvent interactions in the BACS keyboard test

    userEvent v14 returns a promise from every interaction. These tab calls
    were not awaited, so each focus assertion ran against whatever state the
    previous move happened to have reached — passing by timing rather than by
    guarantee. eslint flags this as testing-library/await-async-events.

    Unrelated to the rest of this branch; it surfaced when linting the file
    alongside the change beside it.

    * Add changelog entry for the bank account default country fix

    * test: Cover the last-resort country for new BACS bank accounts

    The country a new bank account starts in walks three sources in order: the
    Payments business location, the store's base country, and a hardcoded 'US'.
    Only the first two were covered, so nothing pinned the behavior when a store
    has no location on record — a fresh install where the globals are absent, or
    one where they are present but stored empty.

    Add a case for each: empty strings must fall through the chain rather than
    being taken as a location, and a missing global must not break the render.

    Refs #67478

    * fix: Guard the BACS account country against non-string settings

    The country a new bank account starts in is read straight off two client
    globals. Both are documented and typed as strings, but both arrive through
    PHP filters, so a third party can put anything in them. Calling split() on
    the store's base country therefore let a non-string value throw during
    render and take the whole BACS settings screen down with it.

    Read both locations through one helper that treats anything other than a
    string as no answer. A bad value now falls through to the next source, and
    ultimately to US, instead of crashing. Routing both reads through it also
    means a business location would get its state suffix stripped too, should
    one ever be stored there.

    Refs #67478

    * test: Cover the fall-through from an unusable business location

    The country chain tries the business location, then the store's base
    country, then US. Tests covered each link on its own: one asserted the
    fall-through from a *missing* business location, others asserted that
    empty and non-string values are rejected. None asserted a rejected value
    falling through to a valid store country.

    That left a real gap. An implementation that branches on whether the
    business location is set, rather than on whether it yields a country,
    passes all of those tests while sending stores with a malformed business
    location to US instead of their own country. Mutation-testing that shape
    against the suite confirmed it survived.

    The new test asserts GB rather than US on purpose, so the assertion
    cannot be satisfied by the last-resort fallback at the end of the chain.

    Refs #67478

diff --git a/plugins/woocommerce/changelog/fix-bacs-account-default-country b/plugins/woocommerce/changelog/fix-bacs-account-default-country
new file mode 100644
index 00000000000..34bd8496d3b
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-bacs-account-default-country
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Start new bank accounts on the Direct bank transfer settings page in the business location set on the Payments settings screen. The store base country used before could arrive with a state suffix ("US:CA"), which matched no entry in the country field and no routing number format.
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-bacs.tsx b/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-bacs.tsx
index 4551172a21f..5939d979bdf 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-bacs.tsx
+++ b/plugins/woocommerce/client/admin/client/settings-payments/offline/settings-payments-bacs.tsx
@@ -28,13 +28,39 @@ import {
 	type OfflineFormValues,
 } from './dataform-controls';

+/**
+ * Reads a country code out of a location setting.
+ *
+ * These settings reach the client through filters, so treat anything that is
+ * not a string as no answer rather than assuming the declared type holds. A
+ * stored base location can also carry a state suffix ('US:CA'), which matches
+ * neither the country field's options nor the routing-number rules, so keep
+ * only the country.
+ *
+ * @param value The stored setting value.
+ * @return The ISO 3166-1 alpha-2 country code, or an empty string if there is none.
+ */
+const toCountryCode = ( value: unknown ): string =>
+	typeof value === 'string' ? value.split( ':' )[ 0 ] : '';
+
 /**
  * This page is used to manage the settings for the BACS (Direct bank transfer) payment gateway.
  */
 export const SettingsPaymentsBacs = () => {
-	const storeCountryCode =
-		window.wcSettings?.admin?.preloadSettings?.general
-			?.woocommerce_default_country || 'US';
+	// The Payments settings header lets the merchant set a business location
+	// independently of the store address, and keeps this global in sync when
+	// they change it. Start a new account there, since it is the more recent
+	// statement of where they bank. The store's base country is the fallback.
+	const defaultAccountCountry =
+		toCountryCode(
+			window.wcSettings?.admin?.woocommerce_payments_nox_profile
+				?.business_country_code
+		) ||
+		toCountryCode(
+			window.wcSettings?.admin?.preloadSettings?.general
+				?.woocommerce_default_country
+		) ||
+		'US';

 	const { createSuccessNotice, createErrorNotice } =
 		useDispatch( 'core/notices' );
@@ -257,7 +283,7 @@ export const SettingsPaymentsBacs = () => {
 									setAccounts( bankAccounts );
 									setHasChanges( true );
 								} }
-								defaultCountry={ storeCountryCode }
+								defaultCountry={ defaultAccountCountry }
 							/>
 						) }
 					</Settings.Section>
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/offline/test/settings-payments-bacs.test.tsx b/plugins/woocommerce/client/admin/client/settings-payments/offline/test/settings-payments-bacs.test.tsx
index 1cebc50602c..b6eccb713f7 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/offline/test/settings-payments-bacs.test.tsx
+++ b/plugins/woocommerce/client/admin/client/settings-payments/offline/test/settings-payments-bacs.test.tsx
@@ -17,7 +17,12 @@ jest.mock( '@wordpress/data', () => ( {
 } ) );

 jest.mock( '~/settings-payments/components/bank-accounts-list', () => ( {
-	BankAccountsList: () => <div data-testid="bank-accounts-list" />,
+	BankAccountsList: ( { defaultCountry }: { defaultCountry: string } ) => (
+		<div
+			data-testid="bank-accounts-list"
+			data-default-country={ defaultCountry }
+		/>
+	),
 } ) );

 const bacsSettings = {
@@ -90,6 +95,116 @@ describe( 'SettingsPaymentsBacs', () => {
 		).toBeInTheDocument();
 	} );

+	describe( 'country a new bank account starts in', () => {
+		const setWcSettings = ( admin: unknown ) => {
+			Object.defineProperty( window, 'wcSettings', {
+				value: { admin },
+				writable: true,
+			} );
+		};
+
+		afterEach( () => {
+			Object.defineProperty( window, 'wcSettings', {
+				value: undefined,
+				writable: true,
+			} );
+		} );
+
+		it( 'uses the business location set on the Payments settings screen', () => {
+			setWcSettings( {
+				woocommerce_payments_nox_profile: {
+					business_country_code: 'TN',
+				},
+				preloadSettings: {
+					general: { woocommerce_default_country: 'US:CA' },
+				},
+			} );
+
+			render( <SettingsPaymentsBacs /> );
+
+			expect(
+				screen.getByTestId( 'bank-accounts-list' )
+			).toHaveAttribute( 'data-default-country', 'TN' );
+		} );
+
+		it( "falls back to the store's base country, without its state suffix", () => {
+			setWcSettings( {
+				preloadSettings: {
+					general: { woocommerce_default_country: 'US:CA' },
+				},
+			} );
+
+			render( <SettingsPaymentsBacs /> );
+
+			expect(
+				screen.getByTestId( 'bank-accounts-list' )
+			).toHaveAttribute( 'data-default-country', 'US' );
+		} );
+
+		it( 'ignores locations that are stored empty', () => {
+			setWcSettings( {
+				woocommerce_payments_nox_profile: {
+					business_country_code: '',
+				},
+				preloadSettings: {
+					general: { woocommerce_default_country: '' },
+				},
+			} );
+
+			render( <SettingsPaymentsBacs /> );
+
+			expect(
+				screen.getByTestId( 'bank-accounts-list' )
+			).toHaveAttribute( 'data-default-country', 'US' );
+		} );
+
+		it( 'ignores locations that are not strings', () => {
+			setWcSettings( {
+				woocommerce_payments_nox_profile: {
+					business_country_code: { country: 'TN' },
+				},
+				preloadSettings: {
+					general: { woocommerce_default_country: true },
+				},
+			} );
+
+			render( <SettingsPaymentsBacs /> );
+
+			expect(
+				screen.getByTestId( 'bank-accounts-list' )
+			).toHaveAttribute( 'data-default-country', 'US' );
+		} );
+
+		it( 'falls through an unusable business location to the store', () => {
+			setWcSettings( {
+				woocommerce_payments_nox_profile: {
+					business_country_code: { country: 'TN' },
+				},
+				preloadSettings: {
+					// Deliberately not US, so this cannot be confused with
+					// the last-resort fallback at the end of the chain.
+					general: { woocommerce_default_country: 'GB' },
+				},
+			} );
+
+			render( <SettingsPaymentsBacs /> );
+
+			expect(
+				screen.getByTestId( 'bank-accounts-list' )
+			).toHaveAttribute( 'data-default-country', 'GB' );
+		} );
+
+		it( 'falls back to US when the store knows no location at all', () => {
+			setWcSettings( {} );
+
+			render( <SettingsPaymentsBacs /> );
+
+			expect(
+				screen.getByTestId( 'bank-accounts-list' )
+			).toHaveAttribute( 'data-default-country', 'US' );
+		} );
+	} );
+
 	it( 'renders placeholders while loading', () => {
 		( useSelect as jest.Mock ).mockReturnValue( {
 			bacsSettings: null,
@@ -183,7 +298,7 @@ describe( 'SettingsPaymentsBacs', () => {
 		} );
 	} );

-	it( 'supports keyboard navigation through the form fields', () => {
+	it( 'supports keyboard navigation through the form fields', async () => {
 		render( <SettingsPaymentsBacs /> );

 		// Make a change first so the Save button is enabled (and tabbable).
@@ -191,17 +306,17 @@ describe( 'SettingsPaymentsBacs', () => {
 			target: { value: 'Edited title' },
 		} );

-		userEvent.tab();
+		await userEvent.tab();
 		expect(
 			screen.getByLabelText( 'Enable direct bank transfers' )
 		).toHaveFocus();
-		userEvent.tab();
+		await userEvent.tab();
 		expect( screen.getByLabelText( 'Title' ) ).toHaveFocus();
-		userEvent.tab();
+		await userEvent.tab();
 		expect( screen.getByLabelText( 'Description' ) ).toHaveFocus();
-		userEvent.tab();
+		await userEvent.tab();
 		expect( screen.getByLabelText( 'Instructions' ) ).toHaveFocus();
-		userEvent.tab();
+		await userEvent.tab();
 		expect(
 			screen.getByRole( 'button', { name: 'Save changes' } )
 		).toHaveFocus();