diff --git a/changelog/fix-9046-allow-empty-state-when-optional-afterpay-gateway b/changelog/fix-9046-allow-empty-state-when-optional-afterpay-gateway new file mode 100644 index 00000000000..34b8d13c9d4 --- /dev/null +++ b/changelog/fix-9046-allow-empty-state-when-optional-afterpay-gateway @@ -0,0 +1,4 @@ +Significance: minor +Type: fix + +Allow Afterpay gateway to process payments when the state/county is optional for GB and NZ addresses. diff --git a/includes/class-wc-payment-gateway-wcpay.php b/includes/class-wc-payment-gateway-wcpay.php index e1e6a3543c9..2ee0a00d1a1 100644 --- a/includes/class-wc-payment-gateway-wcpay.php +++ b/includes/class-wc-payment-gateway-wcpay.php @@ -4501,7 +4501,33 @@ private function upe_needs_redirection( $payment_methods ) { * @return void */ private function handle_afterpay_shipping_requirement( WC_Order $order, Create_And_Confirm_Intention $request ): void { - $check_if_usable = function ( array $address ): bool { + $wc_locale_data = WC()->countries->get_country_locale(); + + $check_if_usable = function ( array $address ) use ( $wc_locale_data ): bool { + if ( $address['country'] && isset( $wc_locale_data[ $address['country'] ] ) ) { + $country_locale_data = $wc_locale_data[ $address['country'] ]; + $fields_to_check = [ + 'state' => 'state', + 'city' => 'city', + 'postcode' => 'postal_code', + 'address_1' => 'line1', + ]; + + foreach ( $fields_to_check as $locale_field => $address_field ) { + $is_field_required = ( + ! isset( $country_locale_data[ $locale_field ] ) || + ! isset( $country_locale_data[ $locale_field ]['required'] ) || + $country_locale_data[ $locale_field ]['required'] + ); + + if ( $is_field_required && ! $address[ $address_field ] ) { + return false; + } + } + + return true; + } + return $address['country'] && $address['state'] && $address['city'] && $address['postal_code'] && $address['line1']; }; diff --git a/tests/WCPAY_UnitTestCase.php b/tests/WCPAY_UnitTestCase.php index 4470710b519..d7f38e3b5d0 100644 --- a/tests/WCPAY_UnitTestCase.php +++ b/tests/WCPAY_UnitTestCase.php @@ -114,14 +114,15 @@ public function createMock( string $original_class_name ): MockObject { // phpcs * @param mixed $response The expected response. * @param WC_Payments_API_Client $api_client_mock Specific API client mock if necessary. * @param WC_Payments_Http $http_mock Specific HTTP mock if necessary. + * @param bool $force_request_mock When true, a request will be mocked even if $total_api_calls is 0. * * @return Request|MockObject The mocked request. */ - protected function mock_wcpay_request( string $request_class, int $total_api_calls = 1, $request_class_constructor_id = null, $response = null, $api_client_mock = null, $http_mock = null ) { + protected function mock_wcpay_request( string $request_class, int $total_api_calls = 1, $request_class_constructor_id = null, $response = null, $api_client_mock = null, $http_mock = null, $force_request_mock = false ) { $http_mock = $http_mock ? $http_mock : $this->createMock( WC_Payments_Http::class ); $api_client_mock = $api_client_mock ? $api_client_mock : $this->createMock( WC_Payments_API_Client::class ); - if ( 1 > $total_api_calls ) { + if ( 1 > $total_api_calls && ! $force_request_mock ) { $api_client_mock->expects( $this->never() )->method( 'send_request' ); // No expectation for calls, return here. diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php index 6879500a7b2..dbc0e81876b 100644 --- a/tests/unit/test-class-wc-payment-gateway-wcpay.php +++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php @@ -13,6 +13,7 @@ use WCPay\Core\Server\Request\Get_Charge; use WCPay\Core\Server\Request\Get_Intention; use WCPay\Core\Server\Request\Get_Setup_Intention; +use WCPay\Constants\Country_Code; use WCPay\Constants\Order_Status; use WCPay\Constants\Intent_Status; use WCPay\Constants\Payment_Method; @@ -21,6 +22,7 @@ use WCPay\Exceptions\Amount_Too_Small_Exception; use WCPay\Exceptions\API_Exception; use WCPay\Exceptions\Fraud_Prevention_Enabled_Exception; +use WCPay\Exceptions\Invalid_Address_Exception; use WCPay\Exceptions\Process_Payment_Exception; use WCPay\Exceptions\Order_ID_Mismatch_Exception; use WCPay\Fraud_Prevention\Fraud_Prevention_Service; @@ -188,6 +190,13 @@ class WC_Payment_Gateway_WCPay_Test extends WCPAY_UnitTestCase { */ private $mock_duplicates_detection_service; + /** + * Backup of WC locale data + * + * @var array + */ + private $locale_backup; + /** * Pre-test setup */ @@ -269,6 +278,8 @@ public function set_up() { ->method( 'get_payment_metadata' ) ->willReturn( [] ); wcpay_get_test_container()->replace( OrderService::class, $mock_order_service ); + + $this->locale_backup = WC()->countries->get_country_locale(); } /** @@ -305,6 +316,7 @@ public function tear_down() { wcpay_get_test_container()->reset_all_replacements(); WC()->session->set( 'wc_notices', [] ); + WC()->countries->locale = $this->locale_backup; } public function test_process_redirect_payment_intent_processing() { @@ -2825,6 +2837,120 @@ public function test_process_payment_for_order_upe_payment_method() { $this->card_gateway->process_payment_for_order( WC()->cart, $pi ); } + /** + * @dataProvider process_payment_for_order_afterpay_clearpay_provider + */ + public function test_process_payment_for_order_afterpay_clearpay( array $address, array $locale_data, ?string $expected_exception ) { + $payment_method = 'woocommerce_payments_afterpay_clearpay'; + $expected_upe_payment_method_for_pi_creation = 'afterpay_clearpay'; + $order = WC_Helper_Order::create_order(); + $order->set_currency( 'USD' ); + $order->set_total( 100 ); + $order->set_billing_city( $address['city'] ); + $order->set_billing_state( $address['state'] ); + $order->set_billing_postcode( $address['postcode'] ); + $order->set_billing_country( $address['country'] ); + $order->save(); + + $_POST['wcpay-fraud-prevention-token'] = 'correct-token'; + $_POST['payment_method'] = $payment_method; + $pi = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'afterpay_clearpay' ); + + if ( $expected_exception ) { + $this->mock_wcpay_request( Create_And_Confirm_Intention::class, 0, null, null, null, null, true ); + $this->expectException( $expected_exception ); + } else { + $request = $this->mock_wcpay_request( Create_And_Confirm_Intention::class ); + $request->expects( $this->once() ) + ->method( 'set_payment_methods' ) + ->with( [ $expected_upe_payment_method_for_pi_creation ] ); + $request->expects( $this->once() ) + ->method( 'format_response' ) + ->willReturn( WC_Helper_Intention::create_intention( [ 'status' => 'success' ] ) ); + } + + WC()->countries->locale = $locale_data; + + $afterpay_gateway = current( + array_filter( + $this->gateways, + function ( $gateway ) { + return $gateway->get_payment_method()->get_id() === 'afterpay_clearpay'; + } + ) + ); + + $afterpay_gateway->process_payment_for_order( WC()->cart, $pi ); + } + + public function process_payment_for_order_afterpay_clearpay_provider() { + return [ + 'with valid full address' => [ + 'address' => [ + 'city' => 'WooCity', + 'state' => 'NY', + 'postcode' => '12345', + 'country' => Country_Code::UNITED_STATES, + ], + // An empty locale data means all fields should be required. + 'locale_data' => [], + 'expected_exception' => null, + ], + 'with incomplete address' => [ + 'address' => [ + 'city' => 'WooCity', + 'state' => '', + 'postcode' => '12345', + 'country' => Country_Code::UNITED_STATES, + ], + 'locale_data' => [ + // A missing `required` attribute means that the field will be required. + Country_Code::UNITED_STATES => [ 'state' => [ 'label' => 'State' ] ], + ], + 'expected_exception' => Invalid_Address_Exception::class, + ], + 'optional state' => [ + 'address' => [ + 'city' => 'London', + 'state' => '', + 'postcode' => 'HA9 9LY', + 'country' => Country_Code::UNITED_KINGDOM, + ], + 'locale_data' => [ + Country_Code::UNITED_KINGDOM => [ 'state' => [ 'required' => false ] ], + ], + 'expected_exception' => null, + ], + 'optional state and postcode' => [ + 'address' => [ + 'city' => 'London', + 'state' => '', + 'postcode' => '', + 'country' => Country_Code::UNITED_KINGDOM, + ], + 'locale_data' => [ + Country_Code::UNITED_KINGDOM => [ + 'state' => [ 'required' => false ], + 'postcode' => [ 'required' => false ], + ], + ], + 'expected_exception' => null, + ], + 'optional state, invalid address' => [ + 'address' => [ + 'city' => '', + 'state' => 'London', + 'postcode' => 'HA9 9LY', + 'country' => Country_Code::UNITED_KINGDOM, + ], + 'locale_data' => [ + Country_Code::UNITED_KINGDOM => [ 'state' => [ 'required' => false ] ], + ], + 'expected_exception' => Invalid_Address_Exception::class, + ], + ]; + } + public function test_process_payment_caches_mimimum_amount_and_displays_error_upon_exception() { delete_transient( 'wcpay_minimum_amount_usd' );