Commit a8191ed02de for woocommerce

commit a8191ed02dee43d9986bac549122611c9691cd41
Author: Daniel Mallory <daniel.mallory@automattic.com>
Date:   Thu Jul 2 13:26:23 2026 +0100

    Fix optional WooPayments business structures (#66069)

    * Fix optional WooPayments business structures

    * Add changelog entry for WooPayments onboarding fix

    * Address WooPayments business structure feedback

diff --git a/plugins/woocommerce/changelog/fix-woopayments-optional-business-structure b/plugins/woocommerce/changelog/fix-woopayments-optional-business-structure
new file mode 100644
index 00000000000..0146e5c02c3
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-woopayments-optional-business-structure
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Hide unnecessary WooPayments business structure selection during onboarding.
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/data/business-verification-context.tsx b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/data/business-verification-context.tsx
index 39a4de42030..df29ebdc9eb 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/data/business-verification-context.tsx
+++ b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/data/business-verification-context.tsx
@@ -1,7 +1,7 @@
 /**
  * External dependencies
  */
-import React, { createContext, useContext, useState } from 'react';
+import React, { createContext, useCallback, useContext, useState } from 'react';
 import { isNil, omitBy } from 'lodash';

 /**
@@ -16,16 +16,29 @@ const useBusinessVerificationContextValue = (
 	const [ errors, setErrors ] = useState( {} as OnboardingFields );
 	const [ touched, setTouched ] = useState( {} as OnboardingFields );

+	const updateData = useCallback(
+		( value: Record< string, string | undefined > ) => {
+			setData( ( prev ) => ( { ...prev, ...value } ) );
+		},
+		[]
+	);
+	const updateErrors = useCallback(
+		( value: Record< string, string | undefined > ) => {
+			setErrors( ( prev ) => omitBy( { ...prev, ...value }, isNil ) );
+		},
+		[]
+	);
+	const updateTouched = useCallback( ( value: Record< string, boolean > ) => {
+		setTouched( ( prev ) => ( { ...prev, ...value } ) );
+	}, [] );
+
 	return {
 		data,
-		setData: ( value: Record< string, string | undefined > ) =>
-			setData( ( prev ) => ( { ...prev, ...value } ) ),
+		setData: updateData,
 		errors,
-		setErrors: ( value: Record< string, string | undefined > ) =>
-			setErrors( ( prev ) => omitBy( { ...prev, ...value }, isNil ) ),
+		setErrors: updateErrors,
 		touched,
-		setTouched: ( value: Record< string, boolean > ) =>
-			setTouched( ( prev ) => ( { ...prev, ...value } ) ),
+		setTouched: updateTouched,
 	};
 };

diff --git a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/sections/business-details.tsx b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/sections/business-details.tsx
index 1d0300c5abc..8aa3df1c3bc 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/sections/business-details.tsx
+++ b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/sections/business-details.tsx
@@ -58,34 +58,66 @@ const BusinessDetails: React.FC = () => {
 		( type ) => type.key === data.business_type
 	);

+	const selectedBusinessStructures = selectedBusinessType?.structures ?? [];
+	const shouldDisplayBusinessStructure =
+		selectedBusinessStructures.length > 0 &&
+		selectedBusinessType?.requires_structure !== false &&
+		! (
+			selectedBusinessStructures.length === 1 &&
+			selectedBusinessStructures[ 0 ].key === 'nil'
+		);
+	const selectedBusinessTypeKey = selectedBusinessType?.key;
+	const companyStructure = data[ 'company.structure' ];
+	const latestDataRef = React.useRef( data );
+	latestDataRef.current = data;
+
 	const selectedBusinessStructure =
-		selectedBusinessType?.structures.length === 0 ||
-		selectedBusinessType?.structures.find(
-			( structure ) => structure.key === data[ 'company.structure' ]
+		! shouldDisplayBusinessStructure ||
+		selectedBusinessStructures.find(
+			( structure ) => structure.key === companyStructure
 		);

-	const updateBusinessVerificationData = (
-		selfAssessmentData: OnboardingFields
-	): Promise< void > => {
-		// Update the local state with the new data.
-		setData( selfAssessmentData );
-
-		const saveUrl = currentStep?.actions?.save?.href;
-		if ( saveUrl ) {
-			// Persist the data on the backend.
-			return apiFetch( {
-				url: saveUrl,
-				method: 'POST',
-				data: {
-					self_assessment: selfAssessmentData,
-					source: sessionEntryPoint,
-				},
+	const updateBusinessVerificationData = React.useCallback(
+		( selfAssessmentData: OnboardingFields ): Promise< void > => {
+			// Update the local state with the new data.
+			setData( selfAssessmentData );
+
+			const saveUrl = currentStep?.actions?.save?.href;
+			if ( saveUrl ) {
+				// Persist the data on the backend.
+				return apiFetch( {
+					url: saveUrl,
+					method: 'POST',
+					data: {
+						self_assessment: selfAssessmentData,
+						source: sessionEntryPoint,
+					},
+				} );
+			}
+
+			// Return a resolved promise to maintain consistency with the API.
+			return Promise.resolve();
+		},
+		[ currentStep?.actions?.save?.href, sessionEntryPoint, setData ]
+	);
+
+	React.useEffect( () => {
+		if (
+			selectedBusinessTypeKey &&
+			! shouldDisplayBusinessStructure &&
+			companyStructure !== undefined
+		) {
+			void updateBusinessVerificationData( {
+				...latestDataRef.current,
+				'company.structure': undefined,
 			} );
 		}
-
-		// Return a resolved promise to maintain consistency with the API.
-		return Promise.resolve();
-	};
+	}, [
+		companyStructure,
+		selectedBusinessTypeKey,
+		shouldDisplayBusinessStructure,
+		updateBusinessVerificationData,
+	] );

 	const handleTiedChange = (
 		name: keyof OnboardingFields,
@@ -142,16 +174,15 @@ const BusinessDetails: React.FC = () => {
 					</OnboardingSelectField>
 				</span>
 			) }
-			{ selectedBusinessType &&
-				selectedBusinessType.structures.length > 0 && (
-					<span data-testid={ 'business-structure-select' }>
-						<OnboardingSelectField
-							name="company.structure"
-							options={ selectedBusinessType.structures }
-							onChange={ handleTiedChange }
-						/>
-					</span>
-				) }
+			{ selectedBusinessType && shouldDisplayBusinessStructure && (
+				<span data-testid={ 'business-structure-select' }>
+					<OnboardingSelectField
+						name="company.structure"
+						options={ selectedBusinessStructures }
+						onChange={ handleTiedChange }
+					/>
+				</span>
+			) }
 			{ selectedCountry &&
 				selectedBusinessType &&
 				selectedBusinessStructure && (
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/test/actions.test.ts b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/test/actions.test.ts
new file mode 100644
index 00000000000..91830accfd9
--- /dev/null
+++ b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/test/actions.test.ts
@@ -0,0 +1,56 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+
+/**
+ * Internal dependencies
+ */
+import { createEmbeddedKycSession } from '../utils/actions';
+
+jest.mock( '@wordpress/api-fetch', () => jest.fn() );
+
+const mockApiFetch = apiFetch as jest.Mock;
+
+describe( 'business verification actions', () => {
+	beforeEach( () => {
+		jest.clearAllMocks();
+		mockApiFetch.mockResolvedValue( {
+			session: {
+				clientSecret: 'test-secret',
+				expiresAt: 123,
+				accountId: 'acct_test',
+				isLive: false,
+				accountCreated: true,
+				publishableKey: 'pk_test',
+				locale: 'en_US',
+			},
+		} );
+	} );
+
+	it( 'omits company structure when it is unset', async () => {
+		await createEmbeddedKycSession(
+			{
+				country: 'JP',
+				business_type: 'company',
+				'company.structure': undefined,
+				mcc: 'digital_products__books',
+			},
+			'/wc/v3/payments/onboarding/kyc/session',
+			'settings'
+		);
+
+		expect( mockApiFetch ).toHaveBeenCalledWith( {
+			url: '/wc/v3/payments/onboarding/kyc/session',
+			method: 'POST',
+			data: {
+				self_assessment: {
+					country: 'JP',
+					business_type: 'company',
+					mcc: 'digital_products__books',
+				},
+				source: 'settings',
+			},
+		} );
+	} );
+} );
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/test/business-details-flow.test.tsx b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/test/business-details-flow.test.tsx
index e0c4d2fb12d..f5a9f27cdef 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/test/business-details-flow.test.tsx
+++ b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/test/business-details-flow.test.tsx
@@ -16,6 +16,63 @@ import BusinessDetails from '../sections/business-details';

 jest.mock( '@wordpress/api-fetch', () => jest.fn() );

+jest.mock( '@wordpress/components', () => {
+	const ReactModule = jest.requireActual< typeof import('react') >( 'react' );
+
+	return {
+		Button: ReactModule.forwardRef(
+			(
+				{
+					children,
+					isBusy,
+					variant,
+					...props
+				}: React.ButtonHTMLAttributes< HTMLButtonElement > & {
+					isBusy?: boolean;
+					variant?: string;
+				},
+				ref: React.Ref< HTMLButtonElement >
+			) => (
+				<button
+					{ ...props }
+					ref={ ref }
+					data-busy={ isBusy ? 'true' : undefined }
+					data-variant={ variant }
+				>
+					{ children }
+				</button>
+			)
+		),
+		TextControl: ReactModule.forwardRef(
+			(
+				{
+					label,
+					onChange,
+					value,
+					...props
+				}: React.InputHTMLAttributes< HTMLInputElement > & {
+					label?: string;
+					onChange?: ( value: string ) => void;
+				},
+				ref: React.Ref< HTMLInputElement >
+			) => (
+				<>
+					{ label && <span>{ label }</span> }
+					<input
+						{ ...props }
+						ref={ ref }
+						aria-label={ label }
+						value={ value }
+						onChange={ ( event ) =>
+							onChange?.( event.target.value )
+						}
+					/>
+				</>
+			)
+		),
+	};
+} );
+
 jest.mock( '../../../data/onboarding-context', () => ( {
 	useOnboardingContext: jest.fn(),
 } ) );
@@ -35,7 +92,9 @@ const mockNextStep = jest.fn();

 const fields = {
 	available_countries: {
+		CA: 'Canada',
 		GB: 'United Kingdom (UK)',
+		JP: 'Japan',
 		US: 'United States (US)',
 	},
 	business_types: [
@@ -62,6 +121,23 @@ const fields = {
 				},
 			],
 		},
+		{
+			key: 'CA',
+			name: 'Canada',
+			types: [
+				{
+					key: 'company',
+					name: 'Company',
+					description: '',
+					structures: [
+						{
+							key: 'nil',
+							name: 'None',
+						},
+					],
+				},
+			],
+		},
 		{
 			key: 'GB',
 			name: 'United Kingdom (UK)',
@@ -74,6 +150,30 @@ const fields = {
 				},
 			],
 		},
+		{
+			key: 'JP',
+			name: 'Japan',
+			types: [
+				{
+					key: 'individual',
+					name: 'Individual',
+					description: '',
+					structures: [],
+				},
+				{
+					key: 'company',
+					name: 'Company',
+					description: '',
+					requires_structure: false,
+					structures: [
+						{
+							key: 'sole_proprietorship',
+							name: 'Sole proprietorship',
+						},
+					],
+				},
+			],
+		},
 	],
 	mccs_display_tree: [
 		{
@@ -210,6 +310,78 @@ describe( 'Business details onboarding flow', () => {
 		} );
 	} );

+	it( 'continues without showing optional business structure selection', async () => {
+		renderBusinessDetailsForm( {
+			country: 'JP',
+			business_type: 'company',
+			'company.structure': '',
+			mcc: '5812',
+		} );
+
+		expect(
+			screen.queryByRole( 'combobox', {
+				name: 'What category of legal entity identify your business?',
+			} )
+		).not.toBeInTheDocument();
+		expect(
+			screen.getByRole( 'combobox', {
+				name: /What type of goods or services does your business sell?/,
+			} )
+		).toBeInTheDocument();
+		expect(
+			screen.getByText( /By using WooPayments/ )
+		).toBeInTheDocument();
+
+		await waitFor( () => expect( mockApiFetch ).toHaveBeenCalled() );
+		const savePayload = mockApiFetch.mock.calls.at( -1 )?.[ 0 ].data;
+
+		expect( savePayload ).toEqual( {
+			self_assessment: {
+				country: 'JP',
+				business_type: 'company',
+				'company.structure': undefined,
+				mcc: '5812',
+			},
+			source: 'settings',
+		} );
+	} );
+
+	it( 'continues without showing business structure when nil is the only option', async () => {
+		renderBusinessDetailsForm( {
+			country: 'CA',
+			business_type: 'company',
+			'company.structure': 'nil',
+			mcc: '5812',
+		} );
+
+		expect(
+			screen.queryByRole( 'combobox', {
+				name: 'What category of legal entity identify your business?',
+			} )
+		).not.toBeInTheDocument();
+		expect(
+			screen.getByRole( 'combobox', {
+				name: /What type of goods or services does your business sell?/,
+			} )
+		).toBeInTheDocument();
+		expect(
+			screen.getByText( /By using WooPayments/ )
+		).toBeInTheDocument();
+
+		await waitFor( () => expect( mockApiFetch ).toHaveBeenCalled() );
+		const savePayload = mockApiFetch.mock.calls.at( -1 )?.[ 0 ].data;
+
+		expect( savePayload ).toEqual( {
+			self_assessment: {
+				country: 'CA',
+				business_type: 'company',
+				'company.structure': undefined,
+				mcc: '5812',
+			},
+			source: 'settings',
+		} );
+	} );
+
 	it( 'resets dependent business details when the country changes', async () => {
 		renderBusinessDetailsForm( {
 			business_type: 'company',
diff --git a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/types.ts b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/types.ts
index 6e5c0ab8e8f..bcfffb6e213 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/types.ts
+++ b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/types.ts
@@ -26,6 +26,7 @@ export interface BusinessType {
 	key: string;
 	name: string;
 	description: string;
+	requires_structure?: boolean;
 	structures: BusinessStructure[];
 }

diff --git a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/utils/index.ts b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/utils/index.ts
index 66857b7f8d2..32011082b0e 100644
--- a/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/utils/index.ts
+++ b/plugins/woocommerce/client/admin/client/settings-payments/onboarding/providers/woopayments/steps/business-verification/utils/index.ts
@@ -14,7 +14,9 @@ export const fromDotNotation = (
 	record: Record< string, unknown >
 ): Record< string, unknown > =>
 	toPairs( record ).reduce( ( result, [ key, value ] ) => {
-		return value !== null ? set( result, key, value ) : result;
+		return value !== null && value !== undefined
+			? set( result, key, value )
+			: result;
 	}, {} );

 export const getAvailableCountries = (