Commit e23fc94b861 for woocommerce

commit e23fc94b8619fe3d288d908076ebfd55cba5f6c7
Author: Mayisha <33387139+Mayisha@users.noreply.github.com>
Date:   Thu Mar 5 13:18:27 2026 +0600

    Use paypal standard supported country list from official docs (#63349)

    * add paypal supported countries constant

    * add helper method to check if country is supported by paypal

    * use helper method to check validity of a country code

    * Add changefile(s) from automation for the following project(s): woocommerce

    * add test for normalize_paypal_order_shipping_country_code

    * throw error if shpping address is null but required by PayPal API

    * add test for required shipping sceanrio

    * fix lint

    * fix typo and texts

    * log 2 separate messages

diff --git a/plugins/woocommerce/changelog/63349-add-paypal-standard-supported-country-list-from-docs b/plugins/woocommerce/changelog/63349-add-paypal-standard-supported-country-list-from-docs
new file mode 100644
index 00000000000..21ab71a8f61
--- /dev/null
+++ b/plugins/woocommerce/changelog/63349-add-paypal-standard-supported-country-list-from-docs
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Validate shipping country against PayPal’s supported countries instead of WooCommerce’s full country list to avoid invalid country codes in PayPal requests.
\ No newline at end of file
diff --git a/plugins/woocommerce/src/Gateways/PayPal/Constants.php b/plugins/woocommerce/src/Gateways/PayPal/Constants.php
index f85c2fe7826..f2c2fa6d1d6 100644
--- a/plugins/woocommerce/src/Gateways/PayPal/Constants.php
+++ b/plugins/woocommerce/src/Gateways/PayPal/Constants.php
@@ -123,6 +123,216 @@ class Constants {
 		'RUB', // Russian Ruble.
 	);

+	/**
+	 * Countries supported by PayPal.
+	 * https://developer.paypal.com/reference/country-codes/
+	 *
+	 * @var array<string, string>
+	 */
+	const SUPPORTED_COUNTRIES = array(
+		'AL' => 'Albania',
+		'DZ' => 'Algeria',
+		'AD' => 'Andorra',
+		'AO' => 'Angola',
+		'AI' => 'Anguilla',
+		'AG' => 'Antigua & Barbuda',
+		'AR' => 'Argentina',
+		'AM' => 'Armenia',
+		'AW' => 'Aruba',
+		'AU' => 'Australia',
+		'AT' => 'Austria',
+		'AZ' => 'Azerbaijan',
+		'BS' => 'Bahamas',
+		'BH' => 'Bahrain',
+		'BB' => 'Barbados',
+		'BY' => 'Belarus',
+		'BE' => 'Belgium',
+		'BZ' => 'Belize',
+		'BJ' => 'Benin',
+		'BM' => 'Bermuda',
+		'BT' => 'Bhutan',
+		'BO' => 'Bolivia',
+		'BA' => 'Bosnia & Herzegovina',
+		'BW' => 'Botswana',
+		'BR' => 'Brazil',
+		'VG' => 'British Virgin Islands',
+		'BN' => 'Brunei',
+		'BG' => 'Bulgaria',
+		'BF' => 'Burkina Faso',
+		'BI' => 'Burundi',
+		'KH' => 'Cambodia',
+		'CM' => 'Cameroon',
+		'CA' => 'Canada',
+		'CV' => 'Cape Verde',
+		'KY' => 'Cayman Islands',
+		'TD' => 'Chad',
+		'CL' => 'Chile',
+		'CN' => 'China',
+		'CO' => 'Colombia',
+		'KM' => 'Comoros',
+		'CG' => 'Congo - Brazzaville',
+		'CD' => 'Congo - Kinshasa',
+		'CK' => 'Cook Islands',
+		'CR' => 'Costa Rica',
+		'CI' => 'Côte d\'Ivoire',
+		'HR' => 'Croatia',
+		'CY' => 'Cyprus',
+		'CZ' => 'Czech Republic',
+		'DK' => 'Denmark',
+		'DJ' => 'Djibouti',
+		'DM' => 'Dominica',
+		'DO' => 'Dominican Republic',
+		'EC' => 'Ecuador',
+		'EG' => 'Egypt',
+		'SV' => 'El Salvador',
+		'ER' => 'Eritrea',
+		'EE' => 'Estonia',
+		'ET' => 'Ethiopia',
+		'FK' => 'Falkland Islands',
+		'FO' => 'Faroe Islands',
+		'FJ' => 'Fiji',
+		'FI' => 'Finland',
+		'FR' => 'France',
+		'GF' => 'French Guiana',
+		'PF' => 'French Polynesia',
+		'GA' => 'Gabon',
+		'GM' => 'Gambia',
+		'GE' => 'Georgia',
+		'DE' => 'Germany',
+		'GI' => 'Gibraltar',
+		'GR' => 'Greece',
+		'GL' => 'Greenland',
+		'GD' => 'Grenada',
+		'GP' => 'Guadeloupe',
+		'GT' => 'Guatemala',
+		'GN' => 'Guinea',
+		'GW' => 'Guinea-Bissau',
+		'GY' => 'Guyana',
+		'HN' => 'Honduras',
+		'HK' => 'Hong Kong SAR China',
+		'HU' => 'Hungary',
+		'IS' => 'Iceland',
+		'IN' => 'India',
+		'ID' => 'Indonesia',
+		'IE' => 'Ireland',
+		'IL' => 'Israel',
+		'IT' => 'Italy',
+		'JM' => 'Jamaica',
+		'JP' => 'Japan',
+		'JO' => 'Jordan',
+		'KZ' => 'Kazakhstan',
+		'KE' => 'Kenya',
+		'KI' => 'Kiribati',
+		'KW' => 'Kuwait',
+		'KG' => 'Kyrgyzstan',
+		'LA' => 'Laos',
+		'LV' => 'Latvia',
+		'LS' => 'Lesotho',
+		'LI' => 'Liechtenstein',
+		'LT' => 'Lithuania',
+		'LU' => 'Luxembourg',
+		'MK' => 'Macedonia',
+		'MG' => 'Madagascar',
+		'MW' => 'Malawi',
+		'MY' => 'Malaysia',
+		'MV' => 'Maldives',
+		'ML' => 'Mali',
+		'MT' => 'Malta',
+		'MH' => 'Marshall Islands',
+		'MQ' => 'Martinique',
+		'MR' => 'Mauritania',
+		'MU' => 'Mauritius',
+		'YT' => 'Mayotte',
+		'MX' => 'Mexico',
+		'FM' => 'Micronesia',
+		'MD' => 'Moldova',
+		'MC' => 'Monaco',
+		'MN' => 'Mongolia',
+		'ME' => 'Montenegro',
+		'MS' => 'Montserrat',
+		'MA' => 'Morocco',
+		'MZ' => 'Mozambique',
+		'NA' => 'Namibia',
+		'NR' => 'Nauru',
+		'NP' => 'Nepal',
+		'NL' => 'Netherlands',
+		'NC' => 'New Caledonia',
+		'NZ' => 'New Zealand',
+		'NI' => 'Nicaragua',
+		'NE' => 'Niger',
+		'NG' => 'Nigeria',
+		'NU' => 'Niue',
+		'NF' => 'Norfolk Island',
+		'NO' => 'Norway',
+		'OM' => 'Oman',
+		'PW' => 'Palau',
+		'PA' => 'Panama',
+		'PG' => 'Papua New Guinea',
+		'PY' => 'Paraguay',
+		'PE' => 'Peru',
+		'PH' => 'Philippines',
+		'PN' => 'Pitcairn Islands',
+		'PL' => 'Poland',
+		'PT' => 'Portugal',
+		'QA' => 'Qatar',
+		'RE' => 'Réunion',
+		'RO' => 'Romania',
+		'RU' => 'Russia',
+		'RW' => 'Rwanda',
+		'WS' => 'Samoa',
+		'SM' => 'San Marino',
+		'ST' => 'São Tomé & Príncipe',
+		'SA' => 'Saudi Arabia',
+		'SN' => 'Senegal',
+		'RS' => 'Serbia',
+		'SC' => 'Seychelles',
+		'SL' => 'Sierra Leone',
+		'SG' => 'Singapore',
+		'SK' => 'Slovakia',
+		'SI' => 'Slovenia',
+		'SB' => 'Solomon Islands',
+		'SO' => 'Somalia',
+		'ZA' => 'South Africa',
+		'KR' => 'South Korea',
+		'ES' => 'Spain',
+		'LK' => 'Sri Lanka',
+		'SH' => 'St. Helena',
+		'KN' => 'St. Kitts & Nevis',
+		'LC' => 'St. Lucia',
+		'PM' => 'St. Pierre & Miquelon',
+		'VC' => 'St. Vincent & Grenadines',
+		'SR' => 'Suriname',
+		'SJ' => 'Svalbard & Jan Mayen',
+		'SZ' => 'Swaziland',
+		'SE' => 'Sweden',
+		'CH' => 'Switzerland',
+		'TW' => 'Taiwan',
+		'TJ' => 'Tajikistan',
+		'TZ' => 'Tanzania',
+		'TH' => 'Thailand',
+		'TG' => 'Togo',
+		'TO' => 'Tonga',
+		'TT' => 'Trinidad & Tobago',
+		'TN' => 'Tunisia',
+		'TM' => 'Turkmenistan',
+		'TC' => 'Turks & Caicos Islands',
+		'TV' => 'Tuvalu',
+		'UG' => 'Uganda',
+		'UA' => 'Ukraine',
+		'AE' => 'United Arab Emirates',
+		'GB' => 'United Kingdom',
+		'US' => 'United States',
+		'UY' => 'Uruguay',
+		'VU' => 'Vanuatu',
+		'VA' => 'Vatican City',
+		'VE' => 'Venezuela',
+		'VN' => 'Vietnam',
+		'WF' => 'Wallis & Futuna',
+		'YE' => 'Yemen',
+		'ZM' => 'Zambia',
+		'ZW' => 'Zimbabwe',
+	);
+
 	/**
 	 * PayPal authorization already captured issue code.
 	 *
diff --git a/plugins/woocommerce/src/Gateways/PayPal/Helper.php b/plugins/woocommerce/src/Gateways/PayPal/Helper.php
index 8c8db16ef3b..92d1adc3c55 100644
--- a/plugins/woocommerce/src/Gateways/PayPal/Helper.php
+++ b/plugins/woocommerce/src/Gateways/PayPal/Helper.php
@@ -18,6 +18,16 @@ defined( 'ABSPATH' ) || exit;
  * @since 10.5.0
  */
 class Helper {
+	/**
+	 * Check if a country code is supported by PayPal.
+	 *
+	 * @param string $country_code Country code.
+	 * @return bool True if the country is supported by PayPal, false otherwise.
+	 */
+	public static function is_country_supported_by_paypal( string $country_code ): bool {
+		return array_key_exists( $country_code, PayPalConstants::SUPPORTED_COUNTRIES );
+	}
+
 	/**
 	 * Check if the PayPal gateway is enabled.
 	 *
diff --git a/plugins/woocommerce/src/Gateways/PayPal/Request.php b/plugins/woocommerce/src/Gateways/PayPal/Request.php
index 8613c735d39..992e5ba889b 100644
--- a/plugins/woocommerce/src/Gateways/PayPal/Request.php
+++ b/plugins/woocommerce/src/Gateways/PayPal/Request.php
@@ -8,6 +8,7 @@ use Exception;
 use WC_Order;
 use Automattic\WooCommerce\Gateways\PayPal\Constants as PayPalConstants;
 use Automattic\WooCommerce\Gateways\PayPal\AddressRequirements as PayPalAddressRequirements;
+use Automattic\WooCommerce\Gateways\PayPal\Helper as PayPalHelper;
 use Automattic\WooCommerce\Enums\OrderStatus;
 use Automattic\Jetpack\Connection\Client as Jetpack_Connection_Client;

@@ -771,6 +772,10 @@ class Request {
 		$shipping = $this->get_paypal_order_shipping( $order );
 		if ( $shipping ) {
 			$params['purchase_units'][0]['shipping'] = $shipping;
+		} elseif ( PayPalConstants::SHIPPING_SET_PROVIDED_ADDRESS === $shipping_preference ) {
+			// If the shipping preference is set to SET_PROVIDED_ADDRESS, but no shipping information is provided, PayPal create order request will fail.
+			// Throw an exception to prevent the request from being sent.
+			throw new Exception( 'Shipping address is required for PayPal create-order request. Order ID: ' . esc_html( (string) $order->get_id() ) );
 		}

 		return $params;
@@ -1037,7 +1042,7 @@ class Request {

 		// Check if it's a valid alpha-2 code.
 		if ( strlen( $code ) === PayPalConstants::PAYPAL_COUNTRY_CODE_LENGTH ) {
-			if ( WC()->countries->country_exists( $code ) ) {
+			if ( PayPalHelper::is_country_supported_by_paypal( $code ) ) {
 				return $code;
 			}

@@ -1057,7 +1062,12 @@ class Request {
 		// Check if it's a valid alpha-3 code.
 		$alpha2 = WC()->countries->get_country_from_alpha_3_code( $code );
 		if ( null === $alpha2 ) {
-			\WC_Gateway_Paypal::log( sprintf( 'Invalid alpha-3 country code: %s', $code ) );
+			\WC_Gateway_Paypal::log( sprintf( 'Invalid alpha-3 country code: %s', $code ), 'error' );
+			return null;
+		}
+		if ( ! PayPalHelper::is_country_supported_by_paypal( $alpha2 ) ) {
+			\WC_Gateway_Paypal::log( sprintf( 'Country not supported by PayPal: %s (resolved from alpha-3: %s)', $alpha2, $code ) );
+			return null;
 		}

 		return $alpha2;
diff --git a/plugins/woocommerce/tests/php/src/Gateways/PayPal/RequestTest.php b/plugins/woocommerce/tests/php/src/Gateways/PayPal/RequestTest.php
index 9b9d78a1e6b..1d471bd085e 100644
--- a/plugins/woocommerce/tests/php/src/Gateways/PayPal/RequestTest.php
+++ b/plugins/woocommerce/tests/php/src/Gateways/PayPal/RequestTest.php
@@ -115,6 +115,41 @@ class RequestTest extends \WC_Unit_Test_Case {
 		$this->assertNotNull( $result );
 	}

+	/**
+	 * Test create_paypal_order returns null when shipping preference is SET_PROVIDED_ADDRESS but order has no shipping address.
+	 * No create order request is sent to PayPal in this case.
+	 *
+	 * @return void
+	 */
+	public function test_create_paypal_order_returns_null_when_set_provided_address_but_shipping_country_is_unsupported(): void {
+		$order = \WC_Helper_Order::create_order();
+		$order->set_shipping_country( 'SX' );
+		$order->set_shipping_first_name( 'John' );
+		$order->set_shipping_last_name( 'Doe' );
+		$order->set_shipping_address_1( '123 Main St' );
+		$order->set_shipping_address_2( 'Apt 1' );
+		$order->set_shipping_city( 'Anytown' );
+		$order->set_shipping_state( 'Anystate' );
+		$order->set_shipping_postcode( '12345' );
+		$order->save();
+
+		$previous_settings            = get_option( 'woocommerce_paypal_settings', array() );
+		$settings                     = $previous_settings;
+		$settings['address_override'] = 'yes';
+		$settings['send_shipping']    = 'yes';
+		update_option( 'woocommerce_paypal_settings', $settings );
+
+		add_filter( 'pre_http_request', array( $this, 'create_paypal_order_success' ), 10, 3 );
+
+		$request = new PayPalRequest( new \WC_Gateway_Paypal() );
+		$result  = $request->create_paypal_order( $order );
+
+		remove_filter( 'pre_http_request', array( $this, 'create_paypal_order_success' ) );
+		update_option( 'woocommerce_paypal_settings', $previous_settings );
+
+		$this->assertNull( $result, 'create_paypal_order should return null when SET_PROVIDED_ADDRESS is set but the selected shipping country is unsupported' );
+	}
+
 	/**
 	 * Check that the create_paypal_order params are correct.
 	 *
@@ -375,6 +410,51 @@ class RequestTest extends \WC_Unit_Test_Case {
 		$this->assertEquals( $expected, $result );
 	}

+	// ========================================================================
+	// Tests for normalize_paypal_order_shipping_country_code method
+	// ========================================================================
+
+	/**
+	 * Data provider for normalize_paypal_order_shipping_country_code.
+	 *
+	 * @return array<string, array{string, string|null}>
+	 */
+	public function provider_normalize_paypal_order_shipping_country_code(): array {
+		return array(
+			'alpha2_supported_uppercase'     => array( 'US', 'US' ),
+			'alpha2_supported_lowercase'     => array( 'us', 'US' ),
+			'alpha2_supported_with_space'    => array( ' GB ', 'GB' ),
+			'alpha2_not_supported_by_paypal' => array( 'SX', null ),
+			'alpha2_invalid'                 => array( 'XX', null ),
+			'alpha3_maps_to_supported'       => array( 'USA', 'US' ),
+			'alpha3_maps_to_unsupported'     => array( 'AFG', null ),
+			'alpha3_invalid'                 => array( 'XXX', null ),
+		);
+	}
+
+	/**
+	 * Test normalize_paypal_order_shipping_country_code with various country code scenarios.
+	 *
+	 * @dataProvider provider_normalize_paypal_order_shipping_country_code
+	 *
+	 * @param string      $input    Country code to normalize.
+	 * @param string|null $expected Expected normalized alpha-2 code or null.
+	 *
+	 * @return void
+	 */
+	public function test_normalize_paypal_order_shipping_country_code( string $input, ?string $expected ): void {
+		$gateway = new \WC_Gateway_Paypal();
+		$request = new PayPalRequest( $gateway );
+
+		$reflection = new \ReflectionClass( $request );
+		$method     = $reflection->getMethod( 'normalize_paypal_order_shipping_country_code' );
+		$method->setAccessible( true );
+
+		$result = $method->invokeArgs( $request, array( $input ) );
+
+		$this->assertSame( $expected, $result );
+	}
+
 	// ========================================================================
 	// Tests for capture_authorized_payment method
 	// ========================================================================