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
// ========================================================================