Commit dec898f61c6 for woocommerce
commit dec898f61c6369206047deedd46fb4e6512b8c5e
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Wed Aug 12 12:23:21 2026 +0300
[tests] [Payments NOX] Pin per-country payment suggestion placement with contract tests (#67428)
* Add the per-country payment suggestion placement fixture
The partner recommendations shown in WooCommerce > Settings > Payments are a
transcription of a tracking sheet maintained by Partnerships. Nothing in the
repository recorded what that transcription should be, so the only guard against
it drifting was a pair of data providers asserting a suggestion count per
country.
A count cannot see partner identity, ordering, or section placement. Two of the
six mismatches fixed recently were count-neutral: Andorra failed to promote MONEI
to the primary slot, and Finland ordered Paytrail last. Neither would have failed
any test in the suite.
This adds the contract those tests were missing: one entry per country, naming
the expected partner in each section, in order. Sections mirror the tracking
sheet's own rows, so an entry reads like a sheet column.
The file was generated from the recommendation data at 8b712b1e37, which a full
re-audit established is at parity with the tracking sheet across all 247
countries, and was then verified to reconstruct that audit's independently
produced service output across 494 country/state pairs with no differences.
`primary_offline` records the offline profiler state, which the baseline does not
show; the accompanying test derives that state rather than storing it twice.
* Compare every country's suggestion placement against the fixture
The placement fixture is only a contract if something enforces it. This adds the
test that does.
For each of the 247 countries it projects the real output of
PaymentsProviders::get_extension_suggestions() into the fixture's section shape
and compares, asserting membership and order in every section. Countries whose
entry declares an offline provider are additionally checked in the offline
profiler state, where the expectation is derived from the baseline entry by
moving that partner out of Other and into the offline slot. Deriving it keeps one
entry per country and makes the promotion itself the thing under test.
Preferred slots are assigned by tag and type, never by position in the returned
array. That array is compacted, so index carries no meaning: Venezuela and China
both return a single paypal_full_stack entry, but Venezuela's is an APM and
China's is a type-overridden PSP. Both are pinned as regression tests.
Three guards keep a green run meaningful. A completeness test compares the
fixture's countries against the configured ones in both directions, so a country
added to the source without a fixture entry cannot pass silently. An environment
test fails if a suggested extension is active, since an active extension is
dropped from the suggestions and would read as a missing partner across many
countries at once. And the projection rejects any suggestion whose type or
category maps to no section, rather than letting it disappear from the contract.
Failures print the section deltas and a complete, canonical replacement entry, so
an intentional change is a paste rather than a hand-edit. That output is what
makes hand-maintenance viable and a regeneration command unnecessary.
* Pin PayPal Wallet availability in every Full Stack market
The placement fixture has a blind spot it cannot close on its own. PayPal Wallet
shares a plugin slug with PayPal Full Stack, so wherever Full Stack is surfaced
the Wallet entry is filtered out of the normal view. The fixture therefore never
records it, and removing a country's Wallet entry changes nothing the
per-country comparison can see.
The consequence is a silent break: hiding the preferred PayPal suggestion in such
a country would leave no PayPal at all instead of falling back to an express
checkout, and the whole suite would stay green. Guyana reached exactly that state
before it was corrected, which is what makes this worth guarding rather than
assuming.
This derives the affected markets from the fixture, hides Full Stack through the
real hidden-suggestion path, and requires Wallet to surface as an Other express
checkout in each one. The three markets that deliberately ship without Wallet are
listed explicitly, and a companion test diffs that list against the raw catalog
in both directions so it fails rather than rots when availability changes. A
further test fails if the derived market list is ever empty, so the contract
cannot pass by checking nothing.
* Remove suggestion tests superseded by the placement fixture
With every country's sections pinned, several existing tests now assert things
the fixture already covers, and keeping them means two places to update whenever
a country changes.
The two per-country count providers go first. They were 494 lines and byte
identical to each other: the offline provider asserted the same numbers as the
online one, because Square moving from Other into a preferred slot leaves the
total unchanged. Identity and order imply the count, so the fixture subsumes both
outright.
The rest were hand-written spot checks, each encoding one row of the tracking
sheet for one country: GoCardless ordering in Great Britain, Visa last in Japan,
KOMOJU between Square and Airwallex in Japan, and Mastercard's placement across
its nine primary markets and the UAE. All are now covered systematically.
Helcim's ordering rule is not deleted but relocated, because it is a rule rather
than a country fact. It moves to an invariant over the whole fixture, so adding
Helcim to a new market exercises it automatically instead of requiring someone to
extend a hardcoded list.
What remains tests something the fixture structurally cannot: per-country link
and tag merging, Mercado Pago's per-market URLs, the six profiler permutations
behind Square's offline tags, base detail completeness, and the lookup helpers.
* Add changelog entry for the payment placement contract tests
* Drop the redundant Helcim ordering invariant
The fixture pins the full Other payment provider order for every country,
including both markets where Helcim is suggested, so moving it already fails the
per-country comparison — with a better message, since that one names the section
and prints a replacement entry.
What the invariant uniquely caught was Helcim being added to a new market in a
non-final position. That rests on treating "Helcim last" as a universal rule,
which it never was: it was the placement agreed for the two launch markets. A
guard whose first legitimate failure would be answered by deleting the guard is
not worth the second place to maintain.
Its companion assertion, that Helcim carries no preferred tag, looks at first
like something the fixture cannot see, since a stray tag is inert while another
PSP already occupies the preferred slot. But that is exactly the case where the
tag changes nothing. In a market where it would promote Helcim, the promotion
shows up in the fixture and the comparison catches it.
The rule of thumb this leaves: a rule test earns its place beside the fixture
only when it asserts something the fixture structurally cannot see. The PayPal
Wallet contract meets that bar, because Wallet is filtered out by its shared
plugin slug and never reaches the fixture at all. This one does not.
* Move the placement contract alongside the suggestions it pins
The test and its fixture were filed next to PaymentsProviders because that is the
class they call. That mirrors the invocation surface rather than the subject: the
fixture is a transcription of PaymentsExtensionSuggestions::$country_extensions,
it replaces tests that lived beside that class, and the test body speaks the
suggestions vocabulary far more than the settings one.
Anyone changing a country's recommendations edits the suggestions source, so the
contract that constrains those edits belongs in the same neighbourhood.
The fixture is renamed to say what it holds. A fixtures directory usually
contains inputs, as the neighbouring RemoteFreeExtensions and email editor
fixtures do; this file is the assertion side, and the `expected-` prefix settles
that before anyone opens it.
The class keeps its own file rather than merging into
PaymentsExtensionSuggestionsTest. That one is a unit test built on a mocked
incentives provider with no store state, while this needs a container-wired
service, an administrator, an enabled gateway, and profiler manipulation.
Merging would impose all of that on forty unrelated unit tests.
No behaviour changes: 609 tests, same assertions, and the fixture path printed in
failure messages is updated to match.
* Extract the core PayPal gateway test helpers into a shared trait
Two Settings test classes each carried their own private load/enable/unload
helpers for the WC core PayPal gateway, used to satisfy the
enabled-ecommerce-gateway rule with a real gateway that is not itself a
suggested extension.
The copies had already drifted, and not cosmetically. WC_Payment_Gateways::init()
adds to the loaded list rather than replacing it, so a gateway that stops being
loadable stays behind at its old index. PaymentsProvidersTest emptied the list
first; PaymentsRestControllerIntegrationTest did not, leaving it unable to
observe PayPal being unloaded. Nothing marked either version as the correct one.
Both now use one trait that keeps the reset, with a comment recording why it is
there. Load and enable differed by a single string, so they collapse into one
private setter. The using class supplies the providers service through an
abstract accessor, because PaymentsProvidersTest asserts against its own
instance while the REST test uses the container singleton — clearing the wrong
one would leave stale gateway details behind.
* Remove placement tests the country fixture already covers
Three tests restated facts the per-country comparison already asserts, so a
legitimate placement change failed in several places at once and had to be
answered in several places too.
The Venezuela test asserted, character for character, the array that is
Venezuela's fixture entry; the China test asserted a strict subset of China's.
Both countries already run through test_country_baseline_placement, which
compares the same values with a better message. What they look like they guard
against — a rewrite of project_sections() to positional indexing — the fixture
catches on its own: positional projection puts Venezuela's only preferred
suggestion in the PSP slot, where its entry says APM. The reasoning they carried
is already recorded in the project_sections() docblock, which names both
countries and explains why slots are assigned by tag and type.
The masked-preferred test asserted that KOMOJU in Japan and Mastercard in the
UAE carry no preferred tag. TAG_PREFERRED is read in exactly one place in the
plugin, inside the loop that fills the preferred slots, so a stray tag either
wins a slot — which changes the fixture — or changes nothing observable at all.
This is the rule the Helcim ordering guard was dropped under: a rule test earns
its place beside the fixture only when it asserts something the fixture
structurally cannot see. It is applied uniformly here.
* Simplify the placement test's setup and helpers
The class did a fair amount of work that bought nothing, and hand-rolled a few
things the codebase already provides.
WP_UnitTestCase wraps every test in a database transaction and rolls it back,
so the options and user meta this test writes are already restored for it. The
class saved and restored them anyway, through three properties and a sentinel
object standing in for a missing option. Dropping that also fixes an ordering
mistake it caused: the gateway singleton was being rebuilt before the rollback,
from the hand-restored values rather than the real ones. Only the singleton and
the provider caches outlive a transaction, so those are all tearDown now resets.
The offline check ran once per country to do anything for eight of them. The
other 239 cases revalidated a fixture entry the baseline test had already
validated and then asserted true, which is what the no-op assertion in the
middle of the method was for. It gets its own data provider, plus a guard
against that provider silently emptying, mirroring the PayPal Wallet one.
The rest is reuse. The hand-written list check becomes ArrayUtil::array_is_list(),
already used six times by the class under test. The rule for the baseline view
of a fixture entry lived in two places and is now named once. The suggestion
service was pulled from the container at four call sites. The fixture is
memoised, since require returns a value and so cannot be require_once.
Setting up the core PayPal gateway now comes from the shared trait rather than
a third inline copy.
* Assert Other suggestions do not keep the preferred tag
The projection read placement out of a suggestion and discarded its tags, so a
provider that lost the race for its preferred slot could keep carrying the tag
it lost with and nothing would notice. The fixture records IDs, not tags, and
the placement is unchanged by definition — the suggestion is in Other either
way. The tag is part of the REST payload, so the state is wrong where consumers
can see it.
Deleting the '_remove' override on Mastercard in the UAE produces exactly that:
WooPayments has already taken the preferred PSP slot, Mastercard falls through
to Other with its default preferred tag intact, and every placement assertion
still passes.
Projecting Other now asserts the tag is absent. That holds for all 247 countries
in both profiler states, which is what makes it an invariant rather than a fact
about one country: the '_remove' overrides exist precisely to strip the tag from
preferred-by-default extensions that a country surfaces in Other.
This is something the fixture structurally cannot see, so it belongs beside it —
unlike the per-country ordering and tag checks it replaces, which restated rows
the comparison already covers.
* Guard the typed provider property in tearDown
PHPUnit runs the teardown hooks whenever the requirements check passed, which
happens before setUp(), so a setUp() that throws is still followed by
tearDown(). The provider property is typed with no default and is assigned
partway through setUp(), so reading it unguarded raises "must not be accessed
before initialization" on top of whatever actually failed.
PHPUnit keeps the first exception, so the real cause still surfaces, but the
second error is noise on a run that is already going badly. Both sibling suites
guard the same access.
* Assert the preferred-tag invariant only where it holds
The check lived inside the projection helper, so it ran for every caller —
including the test that hides PayPal Full Stack. It does not hold there.
get_extension_suggestions() gates slot assignment on the suggestion being both
preferred and not hidden, so a hidden preferred suggestion skips the slots
entirely and reaches Other with its tags untouched.
That test passed only by coincidence. The shortcut that drops a hidden Full
Stack applies to APMs, and China is the one country overriding it to a PSP —
which the wallet-fallback provider happens to exclude for an unrelated reason.
A second PSP-type override anywhere would have failed a test that has nothing
to do with preferred tags, with a message about preferred tags.
The invariant now has its own method, called from the two states where nothing
is hidden. Coverage is unchanged, and the mutation it exists to catch — dropping
Mastercard's '_remove' override in the UAE — now reports one failure naming the
country instead of two.
* Rebuild the gateway state once per class, not once per test
setUp() calls enable_core_paypal_pg(), which empties the loaded gateway list,
re-runs init(), and clears the provider cache. The per-test tearDown did the
same work again, and 367 of those 368 rebuilds were immediately discarded by
the next setUp. Only the last one is observable, by the next test class.
That last one was also built from the wrong data. WP's tear_down() rolls the
transaction back but never flushes the object cache — the flush happens in the
next set_up() — so a rebuild in tearDown reads the option values the test just
wrote and constructs a PayPal gateway still marked enabled, which is exactly the
state the old comment claimed it was preventing. What actually protected the
next class was that class's own cache flush.
tear_down_after_class() flushes the cache before this hook body runs, so the
rebuild reads what is really in the database. Doing it there fixes the staleness
and drops the redundant work, which also retires the typed-property guard: there
is no longer a per-test teardown reading a property that setUp may not have
reached.
* Resolve preferred slots from a lookup table
The preferred loop mapped suggestion type to slot through an if/elseif chain
sitting directly above an Other loop that does the same kind of mapping through
the CATEGORY_TO_SECTION constant. Two idioms for one job, in one method.
Naming the mapping turns the APM and express-checkout collapse into data, next
to the category map it mirrors, and leaves the fallthrough as a single null
check with its message unchanged.
* Move the payment gateway reload into the shared trait
Emptying the loaded gateway list before calling init() looks like superstition
unless you know that init() adds to the list rather than replacing it, so a
gateway that stops being loadable otherwise stays behind at its old index. That
reasoning was written down once, in the trait, while the same two statements
appeared bare in four other places across three classes.
The idiom moves into the trait beside the explanation, and it is static so the
class-level teardown can use it too. In unmock_payment_gateways() the standalone
empty before the branch also goes: both arms rebuild the list themselves, so it
was doing nothing.
* docs: Clarify payment placement audit provenance
The placement fixture records when and how its source data was independently verified.
Its header named a clone-local audit harness that is intentionally excluded from the repository, leaving public reviewers with an unresolvable path.
Describe the audit method without naming that path. This keeps the provenance useful while leaving the fixture data and test behavior unchanged.
diff --git a/plugins/woocommerce/changelog/add-payment-placement-contract-tests b/plugins/woocommerce/changelog/add-payment-placement-contract-tests
new file mode 100644
index 00000000000..f189742a86b
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-payment-placement-contract-tests
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Add per-country, per-section contract tests pinning the Payments settings partner recommendations, replacing two duplicated suggestion-count data providers and several per-country spot tests.
diff --git a/plugins/woocommerce/tests/legacy/bootstrap.php b/plugins/woocommerce/tests/legacy/bootstrap.php
index 3ea22eb152a..40fac14d320 100644
--- a/plugins/woocommerce/tests/legacy/bootstrap.php
+++ b/plugins/woocommerce/tests/legacy/bootstrap.php
@@ -349,6 +349,7 @@ class WC_Unit_Tests_Bootstrap {
require_once dirname( $this->tests_dir ) . '/php/helpers/SerializingCacheTrait.php';
require_once dirname( $this->tests_dir ) . '/php/helpers/LoggerSpyTrait.php';
require_once dirname( $this->tests_dir ) . '/php/helpers/MetaDataAssertionTrait.php';
+ require_once dirname( $this->tests_dir ) . '/php/helpers/CorePayPalGatewayTrait.php';
}
/**
diff --git a/plugins/woocommerce/tests/php/helpers/CorePayPalGatewayTrait.php b/plugins/woocommerce/tests/php/helpers/CorePayPalGatewayTrait.php
new file mode 100644
index 00000000000..b3067a7f469
--- /dev/null
+++ b/plugins/woocommerce/tests/php/helpers/CorePayPalGatewayTrait.php
@@ -0,0 +1,89 @@
+<?php
+/**
+ * CorePayPalGatewayTrait file.
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\RestApi\UnitTests;
+
+use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders;
+
+/**
+ * Trait CorePayPalGatewayTrait.
+ *
+ * Controls the WC core PayPal gateway for tests that need a real, non-suggested
+ * payment gateway present — for example to satisfy the enabled-ecommerce-gateway
+ * rule in the payment provider and suggestion services.
+ *
+ * The using class must expose the payment providers service it asserts against,
+ * because that service caches gateway details and would otherwise keep serving
+ * the state from before the gateway changed.
+ */
+trait CorePayPalGatewayTrait {
+
+ /**
+ * The payment providers service whose cache these helpers must invalidate.
+ *
+ * @return PaymentsProviders
+ */
+ abstract protected function get_payments_providers_service(): PaymentsProviders;
+
+ /**
+ * Load the WC core PayPal gateway without enabling it.
+ */
+ protected function load_core_paypal_pg(): void {
+ $this->set_core_paypal_pg_enabled( false );
+ }
+
+ /**
+ * Load and enable the WC core PayPal gateway.
+ */
+ protected function enable_core_paypal_pg(): void {
+ $this->set_core_paypal_pg_enabled( true );
+ }
+
+ /**
+ * Clean up the WC core PayPal gateway.
+ */
+ protected function unload_core_paypal_pg(): void {
+ delete_option( 'woocommerce_paypal_settings' );
+ delete_option( 'woocommerce_currency' );
+
+ $this->get_payments_providers_service()->clear_cache();
+ }
+
+ /**
+ * Write the core PayPal gateway settings and reload the gateway list.
+ *
+ * @param bool $enabled Whether the gateway should be enabled.
+ */
+ private function set_core_paypal_pg_enabled( bool $enabled ): void {
+ update_option(
+ 'woocommerce_paypal_settings',
+ array(
+ '_should_load' => 'yes',
+ 'enabled' => $enabled ? 'yes' : 'no',
+ )
+ );
+ // Make sure the store currency is supported by the gateway.
+ update_option( 'woocommerce_currency', 'USD' );
+
+ self::reload_payment_gateways();
+
+ // Clear cached provider data to pick up the new gateway details.
+ $this->get_payments_providers_service()->clear_cache();
+ }
+
+ /**
+ * Rebuild the loaded payment gateways from the current options.
+ *
+ * WC_Payment_Gateways::init() adds to the loaded list rather than replacing
+ * it, so the list has to be emptied first for a gateway that is no longer
+ * loadable to actually disappear.
+ */
+ protected static function reload_payment_gateways(): void {
+ WC()->payment_gateways()->payment_gateways = array();
+ WC()->payment_gateways()->init();
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php
index 05d27571588..6a0df086ce5 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsProvidersTest.php
@@ -8,6 +8,7 @@ use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\PaymentGate
use Automattic\WooCommerce\Internal\Admin\Settings\Payments;
use Automattic\WooCommerce\Internal\Admin\Suggestions\PaymentsExtensionSuggestions as ExtensionSuggestions;
use Automattic\WooCommerce\Proxies\LegacyProxy;
+use Automattic\WooCommerce\RestApi\UnitTests\CorePayPalGatewayTrait;
use Automattic\WooCommerce\Tests\Internal\Admin\Settings\Mocks\FakePaymentGateway;
use PHPUnit\Framework\MockObject\MockObject;
use WC_Unit_Test_Case;
@@ -22,6 +23,7 @@ use WC_Gateway_Paypal;
* @class PaymentsProviders
*/
class PaymentsProvidersTest extends WC_Unit_Test_Case {
+ use CorePayPalGatewayTrait;
/**
* @var PaymentsProviders
@@ -66,8 +68,7 @@ class PaymentsProvidersTest extends WC_Unit_Test_Case {
public function tearDown(): void {
// Reset gateways, hooks, and cached provider data between tests.
remove_all_actions( 'wc_payment_gateways_initialized' );
- WC()->payment_gateways()->payment_gateways = array();
- WC()->payment_gateways()->init();
+ self::reload_payment_gateways();
if ( isset( $this->sut ) ) {
$this->sut->clear_cache();
}
@@ -6148,59 +6149,12 @@ class PaymentsProvidersTest extends WC_Unit_Test_Case {
}
/**
- * Load the WC core PayPal gateway but not enable it.
- *
- * @return void
- */
- private function load_core_paypal_pg() {
- // Make sure the WC core PayPal gateway is loaded.
- update_option(
- 'woocommerce_paypal_settings',
- array(
- '_should_load' => 'yes',
- 'enabled' => 'no',
- )
- );
- // Make sure the store currency is supported by the gateway.
- update_option( 'woocommerce_currency', 'USD' );
- WC()->payment_gateways()->payment_gateways = array();
- WC()->payment_gateways()->init();
-
- // Clear cached provider data to pick up the new gateway details.
- $this->sut->clear_cache();
- }
-
- /**
- * Enable the WC core PayPal gateway.
+ * The payment providers service the core PayPal gateway helpers must invalidate.
*
- * @return void
- */
- private function enable_core_paypal_pg() {
- // Enable the WC core PayPal gateway.
- update_option(
- 'woocommerce_paypal_settings',
- array(
- '_should_load' => 'yes',
- 'enabled' => 'yes',
- )
- );
- // Make sure the store currency is supported by the gateway.
- update_option( 'woocommerce_currency', 'USD' );
- WC()->payment_gateways()->payment_gateways = array();
- WC()->payment_gateways()->init();
-
- // Clear cached provider data to pick up the new gateway details.
- $this->sut->clear_cache();
- }
-
- /**
- * Cleanup the core PayPal gateway.
+ * @return PaymentsProviders
*/
- private function unload_core_paypal_pg() {
- delete_option( 'woocommerce_paypal_settings' );
- delete_option( 'woocommerce_currency' );
-
- $this->sut->clear_cache();
+ protected function get_payments_providers_service(): PaymentsProviders {
+ return $this->sut;
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php
index 03003feb1b2..7f3525d6e9f 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Settings/PaymentsRestControllerIntegrationTest.php
@@ -15,6 +15,7 @@ use Automattic\WooCommerce\Internal\Admin\Suggestions\Incentives\Incentive;
use Automattic\WooCommerce\Internal\Admin\Suggestions\PaymentsExtensionSuggestions;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
use Automattic\WooCommerce\Proxies\LegacyProxy;
+use Automattic\WooCommerce\RestApi\UnitTests\CorePayPalGatewayTrait;
use Automattic\WooCommerce\Testing\Tools\DependencyManagement\MockableLegacyProxy;
use Automattic\WooCommerce\Testing\Tools\TestingContainer;
use Automattic\WooCommerce\Tests\Internal\Admin\Settings\Mocks\FakePaymentGateway;
@@ -32,6 +33,8 @@ use WC_Gateway_Paypal;
* @class PaymentsRestController
*/
class PaymentsRestControllerIntegrationTest extends WC_Unit_Test_Case {
+ use CorePayPalGatewayTrait;
+
/**
* Endpoint.
*
@@ -1921,43 +1924,12 @@ class PaymentsRestControllerIntegrationTest extends WC_Unit_Test_Case {
}
/**
- * Load the WC core PayPal gateway but not enable it.
- */
- private function load_core_paypal_pg() {
- // Make sure the WC core PayPal gateway is loaded.
- update_option(
- 'woocommerce_paypal_settings',
- array(
- '_should_load' => 'yes',
- 'enabled' => 'no',
- )
- );
- // Make sure the store currency is supported by the gateway.
- update_option( 'woocommerce_currency', 'USD' );
- WC()->payment_gateways()->init();
-
- // Clear cached provider data to pick up the new gateway details.
- $this->providers_service->clear_cache();
- }
-
- /**
- * Enable the WC core PayPal gateway.
+ * The payment providers service the core PayPal gateway helpers must invalidate.
+ *
+ * @return PaymentsProviders
*/
- private function enable_core_paypal_pg() {
- // Enable the WC core PayPal gateway.
- update_option(
- 'woocommerce_paypal_settings',
- array(
- '_should_load' => 'yes',
- 'enabled' => 'yes',
- )
- );
- // Make sure the store currency is supported by the gateway.
- update_option( 'woocommerce_currency', 'USD' );
- WC()->payment_gateways()->init();
-
- // Clear cached provider data to pick up the new gateway details.
- $this->providers_service->clear_cache();
+ protected function get_payments_providers_service(): PaymentsProviders {
+ return $this->providers_service;
}
/**
@@ -1986,8 +1958,7 @@ class PaymentsRestControllerIntegrationTest extends WC_Unit_Test_Case {
// Hook into the payment gateways initialization to mock the gateways.
add_action( 'wc_payment_gateways_initialized', $this->gateways_mock_ref, 100 );
// Reinitialize the WC gateways.
- WC()->payment_gateways()->payment_gateways = array();
- WC()->payment_gateways()->init();
+ self::reload_payment_gateways();
$this->providers_service->clear_cache();
@@ -2090,11 +2061,10 @@ class PaymentsRestControllerIntegrationTest extends WC_Unit_Test_Case {
remove_action( 'wc_payment_gateways_initialized', $this->gateways_mock_ref, 100 );
}
// Reinitialize the WC gateways.
- WC()->payment_gateways()->payment_gateways = array();
if ( isset( $this->providers_service ) ) {
$this->load_core_paypal_pg();
} else {
- WC()->payment_gateways()->init();
+ self::reload_payment_gateways();
}
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestionsCountryPlacementTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestionsCountryPlacementTest.php
new file mode 100644
index 00000000000..c85caa565e5
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestionsCountryPlacementTest.php
@@ -0,0 +1,742 @@
+<?php
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\Admin\Suggestions;
+
+use Automattic\WooCommerce\Admin\PluginsHelper;
+use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
+use Automattic\WooCommerce\Internal\Admin\Settings\Payments;
+use Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders;
+use Automattic\WooCommerce\Internal\Admin\Settings\Utils;
+use Automattic\WooCommerce\Internal\Admin\Suggestions\PaymentsExtensionSuggestions;
+use Automattic\WooCommerce\Internal\Utilities\ArrayUtil;
+use Automattic\WooCommerce\RestApi\UnitTests\CorePayPalGatewayTrait;
+use ReflectionClass;
+use WC_Unit_Test_Case;
+
+/**
+ * Per-country, per-section payment suggestion placement contract test.
+ *
+ * @class PaymentsProviders
+ */
+class PaymentsExtensionSuggestionsCountryPlacementTest extends WC_Unit_Test_Case {
+ use CorePayPalGatewayTrait;
+
+ /**
+ * Fixture section keys, in the tracking sheet's row order.
+ */
+ private const SECTION_KEYS = array(
+ 'primary_psp',
+ 'primary_apm',
+ 'primary_offline',
+ 'other_psp',
+ 'other_express_checkout',
+ 'other_bnpl',
+ 'other_crypto',
+ );
+
+ /**
+ * Suggestion category to fixture section key.
+ */
+ private const CATEGORY_TO_SECTION = array(
+ PaymentsProviders::CATEGORY_PSP => 'other_psp',
+ PaymentsProviders::CATEGORY_EXPRESS_CHECKOUT => 'other_express_checkout',
+ PaymentsProviders::CATEGORY_BNPL => 'other_bnpl',
+ PaymentsProviders::CATEGORY_CRYPTO => 'other_crypto',
+ );
+
+ /**
+ * Full Stack markets where PayPal Wallet is deliberately unavailable.
+ */
+ private const PAYPAL_WALLET_UNAVAILABLE_COUNTRIES = array( 'CN', 'GE', 'KZ' );
+
+ /**
+ * Preferred suggestion type to fixture primary slot.
+ *
+ * The preferred APM slot takes express checkouts too, so both types map to it.
+ */
+ private const TYPE_TO_PRIMARY_SLOT = array(
+ PaymentsExtensionSuggestions::TYPE_PSP => 'primary_psp',
+ PaymentsExtensionSuggestions::TYPE_APM => 'primary_apm',
+ PaymentsExtensionSuggestions::TYPE_EXPRESS_CHECKOUT => 'primary_apm',
+ );
+
+ /**
+ * The loaded placement fixture.
+ *
+ * Memoised because `require` returns a value, so `require_once` cannot be used
+ * and every call would otherwise re-parse the file.
+ *
+ * @var array<string, array>|null
+ */
+ private static ?array $fixture = null;
+
+ /**
+ * System under test.
+ *
+ * @var PaymentsProviders
+ */
+ protected PaymentsProviders $sut;
+
+ /**
+ * The raw suggestion catalog, for the tests that read it directly.
+ *
+ * @var PaymentsExtensionSuggestions
+ */
+ protected PaymentsExtensionSuggestions $suggestions;
+
+ /**
+ * The ID of the store admin user.
+ *
+ * @var int
+ */
+ protected int $store_admin_id;
+
+ /**
+ * Set up the baseline store state.
+ *
+ * Baseline means: an admin who may install plugins, an enabled ecommerce
+ * gateway so Express/BNPL/Crypto suggestions are not suppressed, the
+ * online-equivalent profiler state, nothing hidden, and no saved order.
+ *
+ * Everything written here lands inside the per-test transaction that
+ * WP_UnitTestCase opens, so no option or user meta needs restoring by hand.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->store_admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+ wp_set_current_user( $this->store_admin_id );
+
+ // The online-equivalent profiler state.
+ delete_option( OnboardingProfile::DATA_OPTION );
+
+ $this->suggestions = wc_get_container()->get( PaymentsExtensionSuggestions::class );
+ $this->sut = wc_get_container()->get( PaymentsProviders::class );
+
+ // Satisfy the enabled-ecommerce-gateway rule, which otherwise suppresses the
+ // Express/BNPL/Crypto suggestions. Core PayPal is not a suggested extension,
+ // so it does not suppress the PayPal suggestions. This also clears the cache.
+ $this->enable_core_paypal_pg();
+ }
+
+ /**
+ * The payment providers service the core PayPal gateway helpers must invalidate.
+ *
+ * @return PaymentsProviders
+ */
+ protected function get_payments_providers_service(): PaymentsProviders {
+ return $this->sut;
+ }
+
+ /**
+ * Restore the in-process state this class leaks into the next one.
+ *
+ * The gateway singleton and the provider caches outlive the per-test
+ * transaction, so they have to be rebuilt — but once, after the class, not
+ * after every test. setUp() rebuilds both anyway, so a per-test rebuild is
+ * thrown away 367 times out of 368.
+ *
+ * This is also the only place the rebuild is correct. tear_down() rolls the
+ * transaction back but does not flush the object cache, so a rebuild there
+ * reads the option values the test just wrote and hands the next class a
+ * PayPal gateway still marked enabled. tear_down_after_class() flushes the
+ * cache before this runs, so init() sees what is actually in the database.
+ */
+ public static function tearDownAfterClass(): void {
+ parent::tearDownAfterClass();
+
+ self::reload_payment_gateways();
+
+ wc_get_container()->get( PaymentsProviders::class )->clear_cache();
+ }
+
+ /**
+ * @testdox The baseline state surfaces the complete Other catalog.
+ */
+ public function test_baseline_state_surfaces_the_complete_other_catalog(): void {
+ $result = $this->sut->get_extension_suggestions( 'US' );
+ $other = array_column( $result['other'], 'id' );
+
+ $this->assertContains(
+ 'amazon_pay',
+ $other,
+ 'With an enabled ecommerce gateway, the non-preferred express checkout must surface. If this fails, the baseline state is not established and every other assertion in this class is meaningless.'
+ );
+ $this->assertContains( 'affirm', $other, 'BNPL suggestions must surface in the baseline state.' );
+
+ $preferred = array_column( $result['preferred'], 'id' );
+ $this->assertContains(
+ 'paypal_full_stack',
+ $preferred,
+ 'Enabling the core PayPal gateway must not suppress the PayPal suggestion.'
+ );
+ }
+
+ /**
+ * @testdox The fixture covers exactly the countries the source configures.
+ */
+ public function test_fixture_covers_every_configured_country(): void {
+ $reflection = new ReflectionClass( PaymentsExtensionSuggestions::class );
+ $property = $reflection->getProperty( 'country_extensions' );
+ $property->setAccessible( true );
+
+ $configured = array_keys( $property->getValue( $this->suggestions ) );
+ $fixtured = array_keys( self::load_fixture() );
+ sort( $configured );
+ sort( $fixtured );
+
+ $missing = array_diff( $configured, $fixtured );
+ $extra = array_diff( $fixtured, $configured );
+
+ $this->assertSame(
+ array(),
+ array_values( $missing ),
+ 'These countries are configured in PaymentsExtensionSuggestions but absent from the fixture, so their placement is untested: ' . implode( ', ', $missing )
+ );
+ $this->assertSame(
+ array(),
+ array_values( $extra ),
+ 'These countries are in the fixture but no longer configured: ' . implode( ', ', $extra )
+ );
+ }
+
+ /**
+ * @testdox No suggested extension is active in the test environment.
+ */
+ public function test_no_suggested_extension_is_active(): void {
+ $suggested_slugs = array();
+ foreach ( array_keys( self::load_fixture() ) as $country ) {
+ foreach ( $this->suggestions->get_country_extensions( $country ) as $extension ) {
+ $slug = $extension['plugin']['slug'] ?? '';
+ if ( '' !== $slug ) {
+ $suggested_slugs[ $slug ] = true;
+ }
+ }
+ }
+
+ $active_suggested = array();
+ foreach ( PluginsHelper::get_active_plugin_slugs() as $active_slug ) {
+ $slug = Utils::normalize_plugin_slug( $active_slug );
+ if ( isset( $suggested_slugs[ $slug ] ) ) {
+ $active_suggested[ $slug ] = true;
+ }
+ }
+ $active_suggested = array_keys( $active_suggested );
+ sort( $active_suggested );
+
+ $this->assertSame(
+ array(),
+ $active_suggested,
+ 'These suggested extensions are active in the test environment: ' . implode( ', ', $active_suggested ) . '. An active suggested extension is dropped from the suggestions, so every country that lists it will fail. Deactivate it, or remove it from the test environment configuration.'
+ );
+ }
+
+ /**
+ * @testdox PayPal Wallet remains available when PayPal Full Stack is hidden.
+ *
+ * @dataProvider data_provider_paypal_wallet_fallback_countries
+ *
+ * @param string $country The country code.
+ */
+ public function test_paypal_wallet_remains_available_when_full_stack_is_hidden( string $country ): void {
+ update_user_meta(
+ $this->store_admin_id,
+ Payments::PAYMENTS_NOX_PROFILE_KEY,
+ array(
+ 'hidden_suggestions' => array(
+ array(
+ 'id' => PaymentsExtensionSuggestions::PAYPAL_FULL_STACK,
+ 'timestamp' => time(),
+ ),
+ ),
+ )
+ );
+ $this->sut->clear_cache();
+
+ $projected = $this->project_sections( $this->sut->get_extension_suggestions( $country ), $country );
+
+ $this->assertContains(
+ PaymentsExtensionSuggestions::PAYPAL_WALLET,
+ $projected['other_express_checkout'] ?? array(),
+ "PayPal Wallet must remain available as an Other express checkout fallback in $country when PayPal Full Stack is hidden."
+ );
+ }
+
+ /**
+ * Data provider yielding fixture countries that expect Full Stack and support Wallet fallback.
+ *
+ * @return array<string, array{string}>
+ */
+ public function data_provider_paypal_wallet_fallback_countries(): array {
+ $cases = array();
+ foreach ( self::load_fixture() as $country => $expected ) {
+ if ( in_array( $country, self::PAYPAL_WALLET_UNAVAILABLE_COUNTRIES, true ) ) {
+ continue;
+ }
+
+ if ( self::fixture_contains_suggestion( $expected, PaymentsExtensionSuggestions::PAYPAL_FULL_STACK ) ) {
+ $cases[ $country ] = array( $country );
+ }
+ }
+
+ return $cases;
+ }
+
+ /**
+ * @testdox The fixture defines countries that expect PayPal Full Stack.
+ */
+ public function test_paypal_wallet_fallback_country_provider_is_not_empty(): void {
+ $this->assertNotEmpty(
+ $this->data_provider_paypal_wallet_fallback_countries(),
+ 'The PayPal Wallet fallback contract is checking nothing because no fixture country expects PayPal Full Stack.'
+ );
+ }
+
+ /**
+ * @testdox Raw PayPal Wallet exclusions match the explicit unavailable countries.
+ */
+ public function test_paypal_wallet_unavailable_countries_match_raw_catalog(): void {
+ $actual_unavailable_countries = array();
+
+ foreach ( self::load_fixture() as $country => $expected ) {
+ if ( ! self::fixture_contains_suggestion( $expected, PaymentsExtensionSuggestions::PAYPAL_FULL_STACK ) ) {
+ continue;
+ }
+
+ $raw_ids = array_column( $this->suggestions->get_country_extensions( $country ), 'id' );
+ if ( ! in_array( PaymentsExtensionSuggestions::PAYPAL_WALLET, $raw_ids, true ) ) {
+ $actual_unavailable_countries[] = $country;
+ }
+ }
+
+ $expected_unavailable_countries = self::PAYPAL_WALLET_UNAVAILABLE_COUNTRIES;
+ sort( $expected_unavailable_countries );
+ sort( $actual_unavailable_countries );
+
+ $no_longer_unavailable = array_diff( $expected_unavailable_countries, $actual_unavailable_countries );
+ $newly_unavailable = array_diff( $actual_unavailable_countries, $expected_unavailable_countries );
+ $no_longer_message = empty( $no_longer_unavailable ) ? '(none)' : implode( ', ', $no_longer_unavailable );
+ $newly_message = empty( $newly_unavailable ) ? '(none)' : implode( ', ', $newly_unavailable );
+
+ $this->assertSame(
+ $expected_unavailable_countries,
+ $actual_unavailable_countries,
+ 'Raw PayPal Wallet availability does not match PAYPAL_WALLET_UNAVAILABLE_COUNTRIES. '
+ . "Countries where Wallet is now available: $no_longer_message. "
+ . "Countries newly missing Wallet: $newly_message."
+ );
+ }
+
+ /**
+ * Data provider yielding one case per country in the fixture.
+ *
+ * @return array<string, array{string, array}>
+ */
+ public function data_provider_country_placements(): array {
+ $cases = array();
+ foreach ( self::load_fixture() as $country => $expected ) {
+ $cases[ $country ] = array( $country, $expected );
+ }
+
+ return $cases;
+ }
+
+ /**
+ * Data provider yielding only the fixture countries that declare an offline provider.
+ *
+ * @return array<string, array{string, array}>
+ */
+ public function data_provider_offline_country_placements(): array {
+ $cases = array();
+ foreach ( self::load_fixture() as $country => $expected ) {
+ if ( array_key_exists( 'primary_offline', $expected ) ) {
+ $cases[ $country ] = array( $country, $expected );
+ }
+ }
+
+ return $cases;
+ }
+
+ /**
+ * @testdox The fixture defines countries that declare an offline provider.
+ */
+ public function test_offline_country_provider_is_not_empty(): void {
+ $this->assertNotEmpty(
+ $this->data_provider_offline_country_placements(),
+ 'The offline promotion contract is checking nothing because no fixture country declares an offline provider.'
+ );
+ }
+
+ /**
+ * @testdox Each country's baseline placement matches the fixture.
+ *
+ * @dataProvider data_provider_country_placements
+ *
+ * @param string $country The country code.
+ * @param array $expected The expected section map.
+ */
+ public function test_country_baseline_placement( string $country, array $expected ): void {
+ $result = $this->sut->get_extension_suggestions( $country );
+ $projected = $this->project_sections( $result, $country );
+ $this->assert_no_preferred_tag_in_other( $result, $country );
+ $this->assert_valid_section_map( $expected, $country, 'fixture' );
+ $this->assert_valid_section_map( $projected, $country, 'code projection' );
+
+ // The offline slot is filled only in the offline profiler state, so it is
+ // not part of the baseline expectation. Assert it is genuinely empty here
+ // and check the promotion separately.
+ $this->assertArrayNotHasKey(
+ 'primary_offline',
+ $projected,
+ "For $country, the offline slot must be empty in the baseline state."
+ );
+
+ $this->assertSame(
+ $this->normalise( self::baseline_of( $expected ) ),
+ $this->normalise( $projected ),
+ $this->describe_mismatch( $country, $expected, $projected )
+ );
+ }
+
+ /**
+ * @testdox Countries with an offline provider promote it in the offline state.
+ *
+ * @dataProvider data_provider_offline_country_placements
+ *
+ * @param string $country The country code.
+ * @param array $expected The expected section map.
+ */
+ public function test_country_offline_placement( string $country, array $expected ): void {
+ $offline_id = $expected['primary_offline'];
+ $this->assertContains(
+ $offline_id,
+ $expected['other_psp'] ?? array(),
+ "The fixture declares '$offline_id' as $country's offline provider, but it is absent from that entry's other_psp. The offline expectation is derived by moving it out of other_psp, so the entry is inconsistent — fix the fixture."
+ );
+
+ update_option(
+ OnboardingProfile::DATA_OPTION,
+ array(
+ 'business_choice' => 'im_already_selling',
+ 'selling_online_answer' => 'no_im_selling_offline',
+ )
+ );
+ $this->sut->clear_cache();
+
+ $result = $this->sut->get_extension_suggestions( $country );
+ $projected = $this->project_sections( $result, $country );
+ $derived = $expected;
+ $this->assert_no_preferred_tag_in_other( $result, $country );
+ $derived['other_psp'] = array_values(
+ array_filter(
+ $expected['other_psp'],
+ static function ( string $suggestion_id ) use ( $offline_id ): bool {
+ return $offline_id !== $suggestion_id;
+ }
+ )
+ );
+
+ $this->assert_valid_section_map( $derived, $country, 'derived offline expectation' );
+ $this->assert_valid_section_map( $projected, $country, 'offline code projection' );
+ $this->assertSame(
+ $this->normalise( $derived ),
+ $this->normalise( $projected ),
+ "Offline placement for $country does not match the expectation derived from its fixture entry. Fix the baseline entry first — the offline view is derived from it by moving '$offline_id' out of other_psp."
+ );
+ }
+
+ /**
+ * Project a get_extension_suggestions() result into the fixture's section shape.
+ *
+ * Preferred slots are assigned by tag and type, never by array position: the
+ * preferred list is compacted, so index carries no meaning. VE and CN both
+ * return a single 'paypal_full_stack' entry, but VE's is an APM and CN's is a
+ * type-overridden PSP.
+ *
+ * @param array $result The service result.
+ * @param string $country The country code, for failure messages.
+ *
+ * @return array The projected section map.
+ */
+ private function project_sections( array $result, string $country ): array {
+ $projected = array();
+
+ foreach ( $result['preferred'] as $suggestion ) {
+ $tags = $suggestion['tags'] ?? array();
+
+ $slot = in_array( PaymentsExtensionSuggestions::TAG_PREFERRED_OFFLINE, $tags, true )
+ ? 'primary_offline'
+ : ( self::TYPE_TO_PRIMARY_SLOT[ $suggestion['_type'] ] ?? null );
+
+ if ( null === $slot ) {
+ $this->fail(
+ "For $country, the preferred suggestion '{$suggestion['id']}' has type '{$suggestion['_type']}', which maps to no fixture slot."
+ );
+ }
+
+ $this->assertArrayNotHasKey(
+ $slot,
+ $projected,
+ "For $country, the '$slot' slot was filled twice; the second was '{$suggestion['id']}'."
+ );
+
+ $projected[ $slot ] = $suggestion['id'];
+ }
+
+ foreach ( $result['other'] as $suggestion ) {
+ $category = $suggestion['category'] ?? '';
+
+ $this->assertArrayHasKey(
+ $category,
+ self::CATEGORY_TO_SECTION,
+ "For $country, the Other suggestion '{$suggestion['id']}' has category '$category', which maps to no fixture section. An uncategorised suggestion would silently vanish from the contract."
+ );
+
+ $projected[ self::CATEGORY_TO_SECTION[ $category ] ][] = $suggestion['id'];
+ }
+
+ return $projected;
+ }
+
+ /**
+ * Assert that nothing in the Other list kept the tag it lost its slot with.
+ *
+ * The tag is part of the REST payload and the fixture records IDs only, so
+ * nothing else here would notice. Countries that surface a preferred-by-default
+ * extension in Other strip the tag with a '_remove' override.
+ *
+ * This holds only while nothing is hidden. A hidden preferred suggestion skips
+ * the slot assignment entirely and reaches Other with its tags untouched, so
+ * tests that hide a suggestion must not call this.
+ *
+ * @param array $result The service result.
+ * @param string $country The country code, for failure messages.
+ */
+ private function assert_no_preferred_tag_in_other( array $result, string $country ): void {
+ foreach ( $result['other'] as $suggestion ) {
+ $this->assertNotContains(
+ PaymentsExtensionSuggestions::TAG_PREFERRED,
+ $suggestion['tags'] ?? array(),
+ "For $country, the Other suggestion '{$suggestion['id']}' still carries the preferred tag even though another provider filled its preferred slot. Strip it with a '_remove' override on the country entry."
+ );
+ }
+ }
+
+ /**
+ * Assert that a section map follows the placement fixture schema.
+ *
+ * @param array $sections The section map.
+ * @param string $country The country code.
+ * @param string $source The source of the section map.
+ */
+ private function assert_valid_section_map( array $sections, string $country, string $source ): void {
+ $unknown_keys = array_diff( array_keys( $sections ), self::SECTION_KEYS );
+
+ $this->assertSame(
+ array(),
+ $unknown_keys,
+ "For $country, the $source contains unknown section keys: " . implode( ', ', $unknown_keys ) . '.'
+ );
+
+ foreach ( $sections as $key => $value ) {
+ if ( 0 === strpos( $key, 'primary_' ) ) {
+ $this->assertTrue(
+ is_string( $value ) && '' !== $value,
+ "For $country, the $source section '$key' must be a non-empty suggestion ID string."
+ );
+ continue;
+ }
+
+ $is_non_empty_list = is_array( $value )
+ && array() !== $value
+ && ArrayUtil::array_is_list( $value );
+
+ $this->assertTrue(
+ $is_non_empty_list,
+ "For $country, the $source section '$key' must be a non-empty ordered list of suggestion ID strings."
+ );
+
+ foreach ( $value as $index => $suggestion_id ) {
+ $this->assertTrue(
+ is_string( $suggestion_id ) && '' !== $suggestion_id,
+ "For $country, the $source section '$key' item at index $index must be a non-empty suggestion ID string."
+ );
+ }
+ }
+ }
+
+ /**
+ * Order a section map into canonical key order so comparisons are stable.
+ *
+ * @param array $sections The section map.
+ *
+ * @return array The same data in canonical key order.
+ */
+ private function normalise( array $sections ): array {
+ $ordered = array();
+ foreach ( self::SECTION_KEYS as $key ) {
+ if ( array_key_exists( $key, $sections ) ) {
+ $ordered[ $key ] = $sections[ $key ];
+ }
+ }
+
+ return $ordered;
+ }
+
+ /**
+ * Build the assertion message for a country mismatch.
+ *
+ * The message ends with the fixture entry the code currently produces, in
+ * canonical form, so an intentional change can be pasted in directly.
+ *
+ * @param string $country The country code.
+ * @param array $expected The fixture entry.
+ * @param array $projected The projected actual sections.
+ *
+ * @return string
+ */
+ private function describe_mismatch( string $country, array $expected, array $projected ): string {
+ $lines = array(
+ "Placement for $country does not match the fixture.",
+ '(If many countries fail at once, check test_no_suggested_extension_is_active first.)',
+ '',
+ );
+ $baseline_expected = self::baseline_of( $expected );
+
+ foreach ( self::SECTION_KEYS as $key ) {
+ $want = $baseline_expected[ $key ] ?? null;
+ $got = $projected[ $key ] ?? null;
+
+ if ( $want === $got ) {
+ continue;
+ }
+
+ $lines[] = sprintf(
+ ' %s: fixture %s, code %s',
+ $key,
+ null === $want ? '(absent)' : ( is_array( $want ) ? '[' . implode( ', ', $want ) . ']' : "'$want'" ),
+ null === $got ? '(absent)' : ( is_array( $got ) ? '[' . implode( ', ', $got ) . ']' : "'$got'" )
+ );
+ }
+
+ $lines[] = '';
+ $lines[] = 'If this change is intentional, replace the entry with:';
+ $lines[] = '';
+ $lines[] = $this->render_fixture_entry( $country, $projected, $expected['primary_offline'] ?? null );
+ $lines[] = '';
+ $lines[] = 'Then run: pnpm --filter=@woocommerce/plugin-woocommerce lint:php:fix -- tests/php/src/Internal/Admin/Suggestions/fixtures/expected-country-placements.php';
+
+ return implode( "\n", $lines );
+ }
+
+ /**
+ * Render a fixture entry in canonical form, ready to paste.
+ *
+ * @param string $country The country code.
+ * @param array $sections The section map to render.
+ * @param string|null $primary_offline The declared offline provider. It is
+ * preserved only while it remains in the
+ * projected baseline's other_psp list; no
+ * replacement is inferred.
+ *
+ * @return string
+ */
+ private function render_fixture_entry( string $country, array $sections, ?string $primary_offline ): string {
+ if ( null !== $primary_offline && in_array( $primary_offline, $sections['other_psp'] ?? array(), true ) ) {
+ $sections['primary_offline'] = $primary_offline;
+ }
+ $sections = $this->normalise( $sections );
+
+ if ( empty( $sections ) ) {
+ return "\t" . self::render_php_string_literal( $country ) . ' => array(),';
+ }
+
+ $width = max( array_map( 'strlen', array_keys( $sections ) ) );
+ $lines = array( "\t" . self::render_php_string_literal( $country ) . ' => array(' );
+
+ foreach ( $sections as $key => $value ) {
+ $key_literal = self::render_php_string_literal( $key );
+ $pad = str_repeat( ' ', $width - strlen( $key ) );
+
+ if ( is_array( $value ) ) {
+ $lines[] = "\t\t$key_literal$pad => array(";
+ foreach ( $value as $id ) {
+ $lines[] = "\t\t\t" . self::render_php_string_literal( $id ) . ',';
+ }
+ $lines[] = "\t\t),";
+ } else {
+ $lines[] = "\t\t$key_literal$pad => " . self::render_php_string_literal( $value ) . ',';
+ }
+ }
+
+ $lines[] = "\t),";
+
+ return implode( "\n", $lines );
+ }
+
+ /**
+ * Render a value as a PHP single-quoted string literal.
+ *
+ * @param string $value The value to render.
+ *
+ * @return string
+ */
+ private static function render_php_string_literal( string $value ): string {
+ $escaped = str_replace(
+ array( '\\', "'" ),
+ array( '\\\\', "\\'" ),
+ $value
+ );
+
+ return "'$escaped'";
+ }
+
+ /**
+ * The baseline view of a fixture entry.
+ *
+ * The offline slot is filled only in the offline profiler state, so it is not
+ * part of what the baseline comparison expects.
+ *
+ * @param array $sections The fixture section map.
+ *
+ * @return array The same map without the offline slot.
+ */
+ private static function baseline_of( array $sections ): array {
+ unset( $sections['primary_offline'] );
+
+ return $sections;
+ }
+
+ /**
+ * Whether a fixture section map contains a suggestion ID.
+ *
+ * @param array $sections The fixture section map.
+ * @param string $suggestion_id The suggestion ID.
+ *
+ * @return bool
+ */
+ private static function fixture_contains_suggestion( array $sections, string $suggestion_id ): bool {
+ foreach ( $sections as $section ) {
+ $section_ids = is_array( $section ) ? $section : array( $section );
+ if ( in_array( $suggestion_id, $section_ids, true ) ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Load the placement fixture.
+ *
+ * @return array<string, array> Country code to expected section map.
+ */
+ private static function load_fixture(): array {
+ if ( null === self::$fixture ) {
+ self::$fixture = require __DIR__ . '/fixtures/expected-country-placements.php';
+ }
+
+ return self::$fixture;
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestionsTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestionsTest.php
index f63b087b79f..600cdb270cf 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestionsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/PaymentsExtensionSuggestionsTest.php
@@ -58,380 +58,6 @@ class PaymentsExtensionSuggestionsTest extends WC_Unit_Test_Case {
$this->assertNotEmpty( $extensions );
}
- /**
- * Test for each country that we can generate and have the proper number of suggestions when the merchant is selling online.
- *
- * This guards against misconfigurations in the data.
- *
- * @dataProvider data_provider_get_country_extensions_count_with_merchant_selling_online
- *
- * @param string $country The country code.
- * @param int $expected_count The expected number of suggestions.
- */
- public function test_get_country_extensions_count_with_merchant_selling_online( string $country, int $expected_count ) {
- // Merchant is selling online.
- // Arrange.
- update_option(
- OnboardingProfile::DATA_OPTION,
- array(
- 'business_choice' => 'im_already_selling',
- 'selling_online_answer' => 'yes_im_selling_online',
- )
- );
-
- // Act.
- $extensions = $this->sut->get_country_extensions( $country );
-
- // Assert.
- $this->assertCount( $expected_count, $extensions, "For merchant selling online, the country $country should have $expected_count suggestions." );
-
- // Merchant skipped the profiler. We assume they are selling only online.
- // Arrange.
- update_option(
- OnboardingProfile::DATA_OPTION,
- array()
- // No data.
- );
-
- // Act.
- $extensions = $this->sut->get_country_extensions( $country );
-
- // Assert.
- $this->assertCount( $expected_count, $extensions, "For merchant who skipped the profiler, the country $country should have $expected_count suggestions." );
-
- // Merchant didn't answer the profiler questions fully. We assume they are selling only online.
- // Arrange.
- update_option(
- OnboardingProfile::DATA_OPTION,
- array(
- 'business_choice' => 'im_already_selling',
- 'selling_online_answer' => '',
- // No answer.
- )
- );
-
- // Act.
- $extensions = $this->sut->get_country_extensions( $country );
-
- // Assert.
- $this->assertCount( $expected_count, $extensions, "Country $country should have $expected_count suggestions." );
-
- // Clean up.
- delete_option( OnboardingProfile::DATA_OPTION );
- }
-
- /**
- * Data provider for test_get_country_extensions_count_with_merchant_selling_online.
- *
- * @return array
- */
- public function data_provider_get_country_extensions_count_with_merchant_selling_online(): array {
- // The counts are based on the data in PaymentExtensionSuggestions::$country_extensions.
- $country_suggestions_count = array(
- 'CA' => 11,
- 'US' => 12,
- 'GB' => 15,
- 'AT' => 13,
- 'BE' => 11,
- 'BG' => 7,
- 'HR' => 8,
- 'CY' => 9,
- 'CZ' => 8,
- 'DK' => 12,
- 'EE' => 7,
- 'FI' => 11,
- 'FO' => 3,
- 'FR' => 12,
- 'GI' => 4,
- 'DE' => 13,
- 'GR' => 8,
- 'GL' => 3,
- 'HU' => 9,
- 'IE' => 11,
- 'IT' => 10,
- 'LV' => 6,
- 'LI' => 5,
- 'LT' => 7,
- 'LU' => 8,
- 'MT' => 7,
- 'MD' => 3,
- 'NL' => 10,
- 'NO' => 9,
- 'PL' => 9,
- 'PT' => 10,
- 'RO' => 8,
- 'SM' => 3,
- 'SK' => 7,
- 'ES' => 12,
- 'SE' => 10,
- 'CH' => 8,
- 'AG' => 5,
- 'AI' => 3,
- 'AR' => 5,
- 'AW' => 3,
- 'BS' => 5,
- 'BB' => 5,
- 'BZ' => 5,
- 'BM' => 5,
- 'BO' => 2,
- 'BQ' => 3,
- 'BR' => 6,
- 'VG' => 3,
- 'KY' => 5,
- 'CL' => 5,
- 'CO' => 5,
- 'CR' => 5,
- 'CW' => 3,
- 'DM' => 5,
- 'DO' => 5,
- 'EC' => 4,
- 'SV' => 5,
- 'FK' => 2,
- 'GF' => 4,
- 'GD' => 5,
- 'GP' => 4,
- 'GT' => 5,
- 'GY' => 5,
- 'HN' => 5,
- 'JM' => 5,
- 'MQ' => 4,
- 'MX' => 7,
- 'NI' => 5,
- 'PA' => 5,
- 'PY' => 2,
- 'PE' => 5,
- 'KN' => 5,
- 'LC' => 5,
- 'SX' => 3,
- 'VC' => 3,
- 'SR' => 3,
- 'TT' => 5,
- 'TC' => 5,
- 'UY' => 5,
- 'VI' => 3,
- 'VE' => 3,
- 'AU' => 12,
- 'BD' => 2,
- 'CN' => 5,
- 'FJ' => 3,
- 'GU' => 1,
- 'HK' => 8,
- 'IN' => 7,
- 'ID' => 4,
- 'JP' => 9,
- 'MY' => 6,
- 'NC' => 3,
- 'NZ' => 9,
- 'PW' => 3,
- 'PH' => 4,
- 'SG' => 7,
- 'LK' => 2,
- 'KR' => 4,
- 'TH' => 5,
- 'VN' => 4,
- 'DZ' => 3,
- 'AO' => 1,
- 'BJ' => 1,
- 'BW' => 3,
- 'BF' => 1,
- 'BI' => 1,
- 'CM' => 1,
- 'CV' => 1,
- 'CF' => 1,
- 'TD' => 1,
- 'KM' => 1,
- 'CG' => 1,
- 'CI' => 1,
- 'EG' => 5,
- 'CD' => 1,
- 'DJ' => 1,
- 'GQ' => 1,
- 'ER' => 1,
- 'SZ' => 3,
- 'ET' => 1,
- 'GA' => 1,
- 'GH' => 2,
- 'GM' => 1,
- 'GN' => 1,
- 'GW' => 1,
- 'KE' => 3,
- 'LS' => 3,
- 'LR' => 1,
- 'LY' => 1,
- 'MG' => 1,
- 'MW' => 3,
- 'ML' => 1,
- 'MR' => 1,
- 'MU' => 3,
- 'YT' => 1,
- 'MA' => 4,
- 'MZ' => 3,
- 'NA' => 1,
- 'NE' => 1,
- 'NG' => 3,
- 'RE' => 3,
- 'RW' => 1,
- 'ST' => 1,
- 'SN' => 3,
- 'SC' => 3,
- 'SL' => 1,
- 'SO' => 1,
- 'ZA' => 6,
- 'SS' => 1,
- 'TZ' => 1,
- 'TG' => 1,
- 'TN' => 1,
- 'UG' => 1,
- 'EH' => 1,
- 'ZM' => 1,
- 'ZW' => 1,
- 'BH' => 4,
- 'IQ' => 1,
- 'IL' => 2,
- 'JO' => 5,
- 'KW' => 4,
- 'LB' => 1,
- 'OM' => 4,
- 'PK' => 4,
- 'QA' => 4,
- 'SA' => 6,
- 'AE' => 9,
- 'YE' => 1,
- 'AD' => 3,
- 'AF' => 1,
- 'AL' => 2,
- 'AM' => 1,
- 'AQ' => 1,
- 'AS' => 1,
- 'AX' => 1,
- 'AZ' => 1,
- 'BA' => 2,
- 'BL' => 2,
- 'BN' => 1,
- 'BT' => 1,
- 'BV' => 1,
- 'BY' => 1,
- 'CC' => 1,
- 'CK' => 1,
- 'CU' => 1,
- 'CX' => 1,
- 'FM' => 1,
- 'GE' => 2,
- 'GG' => 1,
- 'GS' => 1,
- 'HM' => 1,
- 'HT' => 1,
- 'IM' => 1,
- 'IO' => 1,
- 'IR' => 0,
- 'IS' => 3,
- 'JE' => 1,
- 'KG' => 1,
- 'KH' => 1,
- 'KI' => 1,
- 'KZ' => 2,
- 'LA' => 1,
- 'MC' => 2,
- 'ME' => 1,
- 'MF' => 1,
- 'MH' => 1,
- 'MK' => 1,
- 'MM' => 1,
- 'MN' => 1,
- 'MO' => 1,
- 'MP' => 1,
- 'MS' => 1,
- 'MV' => 1,
- 'NF' => 1,
- 'NP' => 1,
- 'NR' => 1,
- 'NU' => 1,
- 'PF' => 2,
- 'PG' => 1,
- 'PM' => 1,
- 'PN' => 1,
- 'PR' => 2,
- 'PS' => 1,
- 'RS' => 2,
- 'RU' => 1,
- 'SB' => 1,
- 'SD' => 1,
- 'SH' => 1,
- 'SI' => 6,
- 'SJ' => 1,
- 'TF' => 1,
- 'TJ' => 1,
- 'TK' => 1,
- 'TL' => 1,
- 'TM' => 1,
- 'TO' => 1,
- 'TR' => 1,
- 'TV' => 1,
- 'TW' => 2,
- 'UA' => 1,
- 'UM' => 1,
- 'UZ' => 1,
- 'VA' => 1,
- 'VU' => 1,
- 'WF' => 1,
- 'WS' => 1,
- );
-
- $data = array();
- foreach ( $country_suggestions_count as $country => $count ) {
- $data[] = array( $country, $count );
- }
-
- return $data;
- }
-
- /**
- * Test for each country that we can generate and have the proper number of suggestions when the merchant is selling offline.
- *
- * This guards against misconfigurations in the data.
- *
- * @dataProvider data_provider_get_country_extensions_count_with_merchant_selling_offline
- *
- * @param string $country The country code.
- * @param int $expected_count The expected number of suggestions.
- */
- public function test_get_country_extensions_count_with_merchant_selling_offline( string $country, int $expected_count ) {
- // Merchant is selling offline.
- // Arrange.
- update_option(
- OnboardingProfile::DATA_OPTION,
- array(
- 'business_choice' => 'im_already_selling',
- 'selling_online_answer' => 'no_im_selling_offline',
- )
- );
-
- // Act.
- $extensions = $this->sut->get_country_extensions( $country );
-
- // Assert.
- $this->assertCount( $expected_count, $extensions, "For merchant selling offline, the country $country should have $expected_count suggestions." );
-
- // Merchant is selling both online and offline.
- // Arrange.
- update_option(
- OnboardingProfile::DATA_OPTION,
- array(
- 'business_choice' => 'im_already_selling',
- 'selling_online_answer' => 'im_selling_both_online_and_offline',
- )
- );
-
- // Act.
- $extensions = $this->sut->get_country_extensions( $country );
-
- // Assert.
- $this->assertCount( $expected_count, $extensions, "For merchant selling both online and offline, the country $country should have $expected_count suggestions." );
-
- // Clean up.
- delete_option( OnboardingProfile::DATA_OPTION );
- }
-
/**
* @testdox Should tag Square as preferred (and preferred for offline) only when the merchant self-identified as selling offline.
*
@@ -514,291 +140,6 @@ class PaymentsExtensionSuggestionsTest extends WC_Unit_Test_Case {
);
}
- /**
- * Data provider for test_get_country_extensions_count_with_merchant_selling_offline.
- *
- * @return array
- */
- public function data_provider_get_country_extensions_count_with_merchant_selling_offline(): array {
- // The counts are based on the data in PaymentExtensionSuggestions::$country_extensions.
- $country_suggestions_count = array(
- 'CA' => 11,
- 'US' => 12,
- 'GB' => 15,
- 'AT' => 13,
- 'BE' => 11,
- 'BG' => 7,
- 'HR' => 8,
- 'CY' => 9,
- 'CZ' => 8,
- 'DK' => 12,
- 'EE' => 7,
- 'FI' => 11,
- 'FO' => 3,
- 'FR' => 12,
- 'GI' => 4,
- 'DE' => 13,
- 'GR' => 8,
- 'GL' => 3,
- 'HU' => 9,
- 'IE' => 11,
- 'IT' => 10,
- 'LV' => 6,
- 'LI' => 5,
- 'LT' => 7,
- 'LU' => 8,
- 'MT' => 7,
- 'MD' => 3,
- 'NL' => 10,
- 'NO' => 9,
- 'PL' => 9,
- 'PT' => 10,
- 'RO' => 8,
- 'SM' => 3,
- 'SK' => 7,
- 'ES' => 12,
- 'SE' => 10,
- 'CH' => 8,
- 'AG' => 5,
- 'AI' => 3,
- 'AR' => 5,
- 'AW' => 3,
- 'BS' => 5,
- 'BB' => 5,
- 'BZ' => 5,
- 'BM' => 5,
- 'BO' => 2,
- 'BQ' => 3,
- 'BR' => 6,
- 'VG' => 3,
- 'KY' => 5,
- 'CL' => 5,
- 'CO' => 5,
- 'CR' => 5,
- 'CW' => 3,
- 'DM' => 5,
- 'DO' => 5,
- 'EC' => 4,
- 'SV' => 5,
- 'FK' => 2,
- 'GF' => 4,
- 'GD' => 5,
- 'GP' => 4,
- 'GT' => 5,
- 'GY' => 5,
- 'HN' => 5,
- 'JM' => 5,
- 'MQ' => 4,
- 'MX' => 7,
- 'NI' => 5,
- 'PA' => 5,
- 'PY' => 2,
- 'PE' => 5,
- 'KN' => 5,
- 'LC' => 5,
- 'SX' => 3,
- 'VC' => 3,
- 'SR' => 3,
- 'TT' => 5,
- 'TC' => 5,
- 'UY' => 5,
- 'VI' => 3,
- 'VE' => 3,
- 'AU' => 12,
- 'BD' => 2,
- 'CN' => 5,
- 'FJ' => 3,
- 'GU' => 1,
- 'HK' => 8,
- 'IN' => 7,
- 'ID' => 4,
- 'JP' => 9,
- 'MY' => 6,
- 'NC' => 3,
- 'NZ' => 9,
- 'PW' => 3,
- 'PH' => 4,
- 'SG' => 7,
- 'LK' => 2,
- 'KR' => 4,
- 'TH' => 5,
- 'VN' => 4,
- 'DZ' => 3,
- 'AO' => 1,
- 'BJ' => 1,
- 'BW' => 3,
- 'BF' => 1,
- 'BI' => 1,
- 'CM' => 1,
- 'CV' => 1,
- 'CF' => 1,
- 'TD' => 1,
- 'KM' => 1,
- 'CG' => 1,
- 'CI' => 1,
- 'EG' => 5,
- 'CD' => 1,
- 'DJ' => 1,
- 'GQ' => 1,
- 'ER' => 1,
- 'SZ' => 3,
- 'ET' => 1,
- 'GA' => 1,
- 'GH' => 2,
- 'GM' => 1,
- 'GN' => 1,
- 'GW' => 1,
- 'KE' => 3,
- 'LS' => 3,
- 'LR' => 1,
- 'LY' => 1,
- 'MG' => 1,
- 'MW' => 3,
- 'ML' => 1,
- 'MR' => 1,
- 'MU' => 3,
- 'YT' => 1,
- 'MA' => 4,
- 'MZ' => 3,
- 'NA' => 1,
- 'NE' => 1,
- 'NG' => 3,
- 'RE' => 3,
- 'RW' => 1,
- 'ST' => 1,
- 'SN' => 3,
- 'SC' => 3,
- 'SL' => 1,
- 'SO' => 1,
- 'ZA' => 6,
- 'SS' => 1,
- 'TZ' => 1,
- 'TG' => 1,
- 'TN' => 1,
- 'UG' => 1,
- 'EH' => 1,
- 'ZM' => 1,
- 'ZW' => 1,
- 'BH' => 4,
- 'IQ' => 1,
- 'IL' => 2,
- 'JO' => 5,
- 'KW' => 4,
- 'LB' => 1,
- 'OM' => 4,
- 'PK' => 4,
- 'QA' => 4,
- 'SA' => 6,
- 'AE' => 9,
- 'YE' => 1,
- 'AD' => 3,
- 'AF' => 1,
- 'AL' => 2,
- 'AM' => 1,
- 'AQ' => 1,
- 'AS' => 1,
- 'AX' => 1,
- 'AZ' => 1,
- 'BA' => 2,
- 'BL' => 2,
- 'BN' => 1,
- 'BT' => 1,
- 'BV' => 1,
- 'BY' => 1,
- 'CC' => 1,
- 'CK' => 1,
- 'CU' => 1,
- 'CX' => 1,
- 'FM' => 1,
- 'GE' => 2,
- 'GG' => 1,
- 'GS' => 1,
- 'HM' => 1,
- 'HT' => 1,
- 'IM' => 1,
- 'IO' => 1,
- 'IR' => 0,
- 'IS' => 3,
- 'JE' => 1,
- 'KG' => 1,
- 'KH' => 1,
- 'KI' => 1,
- 'KZ' => 2,
- 'LA' => 1,
- 'MC' => 2,
- 'ME' => 1,
- 'MF' => 1,
- 'MH' => 1,
- 'MK' => 1,
- 'MM' => 1,
- 'MN' => 1,
- 'MO' => 1,
- 'MP' => 1,
- 'MS' => 1,
- 'MV' => 1,
- 'NF' => 1,
- 'NP' => 1,
- 'NR' => 1,
- 'NU' => 1,
- 'PF' => 2,
- 'PG' => 1,
- 'PM' => 1,
- 'PN' => 1,
- 'PR' => 2,
- 'PS' => 1,
- 'RS' => 2,
- 'RU' => 1,
- 'SB' => 1,
- 'SD' => 1,
- 'SH' => 1,
- 'SI' => 6,
- 'SJ' => 1,
- 'TF' => 1,
- 'TJ' => 1,
- 'TK' => 1,
- 'TL' => 1,
- 'TM' => 1,
- 'TO' => 1,
- 'TR' => 1,
- 'TV' => 1,
- 'TW' => 2,
- 'UA' => 1,
- 'UM' => 1,
- 'UZ' => 1,
- 'VA' => 1,
- 'VU' => 1,
- 'WF' => 1,
- 'WS' => 1,
- );
-
- $data = array();
- foreach ( $country_suggestions_count as $country => $count ) {
- $data[] = array( $country, $count );
- }
-
- return $data;
- }
-
- /**
- * Test that GoCardless is placed immediately after Klarna Checkout in the GB suggestions order.
- *
- * The order of entries in PaymentsExtensionSuggestions::$country_extensions determines the
- * suggestions' display priority, so this guards against accidental reordering.
- */
- public function test_get_country_extensions_gb_gocardless_order() {
- // Act.
- $extensions = $this->sut->get_country_extensions( 'GB' );
- $ids = array_column( $extensions, 'id' );
-
- // Assert.
- $this->assertCount( 15, $ids );
- $klarna_checkout_index = array_search( PaymentsExtensionSuggestions::KLARNA_CHECKOUT, $ids, true );
- $gocardless_index = array_search( PaymentsExtensionSuggestions::GOCARDLESS, $ids, true );
- $this->assertNotFalse( $klarna_checkout_index, 'Klarna Checkout should be in the GB suggestions.' );
- $this->assertNotFalse( $gocardless_index, 'GoCardless should be in the GB suggestions.' );
- $this->assertSame( $klarna_checkout_index + 1, $gocardless_index, 'GoCardless should immediately follow Klarna Checkout in the GB suggestions.' );
- }
-
/**
* Test getting payment extension suggestions by country with per-country config that uses merges.
*/
@@ -1029,211 +370,6 @@ class PaymentsExtensionSuggestionsTest extends WC_Unit_Test_Case {
);
}
- /**
- * @testdox Helcim is the last PSP suggestion in $country_code.
- *
- * @dataProvider data_provider_helcim_supported_countries
- *
- * @param string $country_code ISO 3166-1 alpha-2 country code.
- */
- public function test_helcim_is_last_psp_suggestion_in_supported_countries( string $country_code ): void {
- $extensions = $this->sut->get_country_extensions( $country_code );
- $extensions_by_id = array_column( $extensions, null, 'id' );
- $psp_ids = array_column(
- array_filter(
- $extensions,
- static fn( array $extension ): bool => PaymentsExtensionSuggestions::TYPE_PSP === $extension['_type']
- ),
- 'id'
- );
- $helcim = $extensions_by_id[ PaymentsExtensionSuggestions::HELCIM ] ?? null;
-
- $this->assertSame(
- PaymentsExtensionSuggestions::HELCIM,
- end( $psp_ids ),
- "Helcim should be the final PSP suggestion in {$country_code}."
- );
- $this->assertIsArray( $helcim, "Helcim should be suggested in {$country_code}." );
- if ( ! is_array( $helcim ) ) {
- return;
- }
-
- $this->assertNotContains(
- PaymentsExtensionSuggestions::TAG_PREFERRED,
- $helcim['tags'],
- "Helcim should remain in other payment options for {$country_code}."
- );
- }
-
- /**
- * @testdox Visa is the last "other payment provider" in Japan.
- */
- public function test_visa_is_last_other_payment_provider_in_jp(): void {
- $extensions = $this->sut->get_country_extensions( 'JP' );
- $psp_ids = array_column(
- array_filter(
- $extensions,
- static fn( array $extension ): bool => PaymentsExtensionSuggestions::TYPE_PSP === $extension['_type']
- ),
- 'id'
- );
-
- $this->assertContains( PaymentsExtensionSuggestions::AIRWALLEX, $psp_ids, 'Airwallex should be suggested in JP.' );
- $this->assertSame(
- PaymentsExtensionSuggestions::VISA,
- end( $psp_ids ),
- 'Visa should be the last PSP suggestion in JP.'
- );
- }
-
- /**
- * @testdox KOMOJU is suggested between Square and Airwallex in Japan.
- */
- public function test_komoju_is_suggested_between_square_and_airwallex_in_jp(): void {
- $extensions = $this->sut->get_country_extensions( 'JP' );
- $extensions_by_id = array_column( $extensions, null, 'id' );
- $psp_ids = array_column(
- array_filter(
- $extensions,
- static fn( array $extension ): bool => PaymentsExtensionSuggestions::TYPE_PSP === $extension['_type']
- ),
- 'id'
- );
- $komoju = $extensions_by_id[ PaymentsExtensionSuggestions::KOMOJU ] ?? null;
-
- $this->assertContains( PaymentsExtensionSuggestions::KOMOJU, $psp_ids, 'KOMOJU should be suggested in JP.' );
- $this->assertSame(
- array_search( PaymentsExtensionSuggestions::SQUARE, $psp_ids, true ) + 1,
- array_search( PaymentsExtensionSuggestions::KOMOJU, $psp_ids, true ),
- 'KOMOJU should immediately follow Square in JP.'
- );
- $this->assertSame(
- array_search( PaymentsExtensionSuggestions::KOMOJU, $psp_ids, true ) + 1,
- array_search( PaymentsExtensionSuggestions::AIRWALLEX, $psp_ids, true ),
- 'KOMOJU should immediately precede Airwallex in JP.'
- );
-
- $this->assertIsArray( $komoju, 'KOMOJU should be suggested in JP.' );
- if ( ! is_array( $komoju ) ) {
- return;
- }
-
- $this->assertNotContains(
- PaymentsExtensionSuggestions::TAG_PREFERRED,
- $komoju['tags'],
- 'KOMOJU should remain in other payment options for JP.'
- );
- }
-
- /**
- * Data provider for Helcim's supported countries.
- *
- * @return array<string, array{string}>
- */
- public function data_provider_helcim_supported_countries(): array {
- return array(
- 'Canada' => array( 'CA' ),
- 'United States' => array( 'US' ),
- );
- }
-
- /**
- * @testdox Mastercard is the first, preferred PSP suggestion in $country_code.
- *
- * Guards the suggestion definition's default TAG_PREFERRED: the tag is declared once
- * on the definition rather than appended per country, so a regression there would be
- * invisible to the count-only assertions.
- *
- * @dataProvider data_provider_mastercard_preferred_countries
- *
- * @param string $country_code ISO 3166-1 alpha-2 country code.
- */
- public function test_mastercard_is_first_and_preferred_in_supported_countries( string $country_code ): void {
- $extensions = $this->sut->get_country_extensions( $country_code );
- $extensions_by_id = array_column( $extensions, null, 'id' );
- $psp_ids = array_column(
- array_filter(
- $extensions,
- static fn( array $extension ): bool => PaymentsExtensionSuggestions::TYPE_PSP === $extension['_type']
- ),
- 'id'
- );
- $mastercard = $extensions_by_id[ PaymentsExtensionSuggestions::MASTERCARD ] ?? null;
-
- $this->assertIsArray( $mastercard, "Mastercard should be suggested in {$country_code}." );
- if ( ! is_array( $mastercard ) ) {
- return;
- }
-
- $this->assertSame(
- PaymentsExtensionSuggestions::MASTERCARD,
- reset( $psp_ids ),
- "Mastercard should be the first PSP suggestion in {$country_code}."
- );
-
- $this->assertContains(
- PaymentsExtensionSuggestions::TAG_PREFERRED,
- $mastercard['tags'],
- "Mastercard should be a preferred suggestion in {$country_code}."
- );
- }
-
- /**
- * Data provider for the countries where Mastercard is a preferred suggestion.
- *
- * @return array Test cases with country codes.
- */
- public function data_provider_mastercard_preferred_countries(): array {
- return array(
- 'Egypt' => array( 'EG' ),
- 'Nigeria' => array( 'NG' ),
- 'South Africa' => array( 'ZA' ),
- 'Bahrain' => array( 'BH' ),
- 'Jordan' => array( 'JO' ),
- 'Kuwait' => array( 'KW' ),
- 'Pakistan' => array( 'PK' ),
- 'Qatar' => array( 'QA' ),
- 'Saudi Arabia' => array( 'SA' ),
- );
- }
-
- /**
- * @testdox Mastercard is suggested but not preferred in the UAE.
- *
- * The UAE is the one market where Mastercard is deliberately not preferred, expressed
- * as a `_remove` of the definition's default tag. Without this the exclusion could be
- * dropped without any test noticing.
- */
- public function test_mastercard_is_not_preferred_in_ae(): void {
- $extensions = $this->sut->get_country_extensions( 'AE' );
- $extensions_by_id = array_column( $extensions, null, 'id' );
- $psp_ids = array_column(
- array_filter(
- $extensions,
- static fn( array $extension ): bool => PaymentsExtensionSuggestions::TYPE_PSP === $extension['_type']
- ),
- 'id'
- );
- $mastercard = $extensions_by_id[ PaymentsExtensionSuggestions::MASTERCARD ] ?? null;
-
- $this->assertIsArray( $mastercard, 'Mastercard should still be suggested in the UAE.' );
- if ( ! is_array( $mastercard ) ) {
- return;
- }
-
- $this->assertNotContains(
- PaymentsExtensionSuggestions::TAG_PREFERRED,
- $mastercard['tags'],
- 'Mastercard should remain in other payment options for the UAE.'
- );
-
- $this->assertNotSame(
- PaymentsExtensionSuggestions::MASTERCARD,
- reset( $psp_ids ),
- 'Mastercard should not lead the UAE PSP suggestions.'
- );
- }
-
/**
* @testdox Helcim has complete base suggestion details.
*/
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/fixtures/expected-country-placements.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/fixtures/expected-country-placements.php
new file mode 100644
index 00000000000..340fb8e8ca3
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/fixtures/expected-country-placements.php
@@ -0,0 +1,1651 @@
+<?php
+/**
+ * Expected payment suggestion placement, per country, per section.
+ *
+ * This is a transcription of the internal partnership tracking sheet. The test
+ * suite proves the code matches this file — it cannot prove this file matches
+ * the sheet. Only a harness run against a fresh export can do that.
+ *
+ * Last verified against the sheet: 2026-08-05, using the regional CSV exports of
+ * that date and a clone-local audit harness.
+ *
+ * Sections mirror the sheet's rows. `primary_offline` describes the offline
+ * profiler state; the baseline state leaves that slot empty and the test derives
+ * the offline expectation from this entry.
+ *
+ * When editing: change entries deliberately, one country at a time, then run
+ * `pnpm --filter=@woocommerce/plugin-woocommerce lint:php:fix -- <this file>`.
+ * Re-verify against the sheet when a re-export lands or partner terms change,
+ * and update the date above only when you have actually done so.
+ *
+ * @package WooCommerce\Tests
+ */
+
+declare( strict_types = 1 );
+
+return array(
+ 'CA' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'primary_offline' => 'square',
+ 'other_psp' => array(
+ 'stripe',
+ 'square',
+ 'visa_as',
+ 'gocardless',
+ 'helcim',
+ ),
+ 'other_bnpl' => array(
+ 'affirm',
+ 'afterpay',
+ 'klarna',
+ ),
+ ),
+ 'PM' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'US' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'primary_offline' => 'square',
+ 'other_psp' => array(
+ 'stripe',
+ 'square',
+ 'visa_as',
+ 'airwallex',
+ 'helcim',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ 'other_bnpl' => array(
+ 'affirm',
+ 'afterpay',
+ 'klarna',
+ ),
+ ),
+ 'UM' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'GB' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'primary_offline' => 'square',
+ 'other_psp' => array(
+ 'stripe',
+ 'square',
+ 'mollie',
+ 'visa_as',
+ 'airwallex',
+ 'viva_wallet',
+ 'klarna_checkout',
+ 'gocardless',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ 'other_bnpl' => array(
+ 'affirm',
+ 'clearpay',
+ 'klarna',
+ ),
+ ),
+ 'AX' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'AL' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_wallet',
+ ),
+ 'AD' => array(
+ 'primary_psp' => 'monei',
+ 'primary_apm' => 'paypal_wallet',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'AM' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'AT' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'airwallex',
+ 'viva_wallet',
+ 'gocardless',
+ 'klarna_checkout',
+ 'nexi_checkout',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'BY' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'BE' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'airwallex',
+ 'viva_wallet',
+ 'gocardless',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'BA' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_wallet',
+ ),
+ 'BV' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'BG' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'viva_wallet',
+ ),
+ ),
+ 'HR' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'viva_wallet',
+ 'gocardless',
+ ),
+ ),
+ 'CY' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'viva_wallet',
+ 'gocardless',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ ),
+ 'CZ' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'viva_wallet',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'DK' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'viva_wallet',
+ 'gocardless',
+ 'klarna_checkout',
+ 'nexi_checkout',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'EE' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'gocardless',
+ ),
+ ),
+ 'FI' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'viva_wallet',
+ 'paytrail',
+ 'gocardless',
+ 'klarna_checkout',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'FO' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'FR' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'primary_offline' => 'square',
+ 'other_psp' => array(
+ 'stripe',
+ 'square',
+ 'mollie',
+ 'visa_as',
+ 'airwallex',
+ 'viva_wallet',
+ 'gocardless',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'PF' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_wallet',
+ ),
+ 'GI' => array(
+ 'primary_psp' => 'stripe',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'DE' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'airwallex',
+ 'viva_wallet',
+ 'gocardless',
+ 'klarna_checkout',
+ 'nexi_checkout',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'GR' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'viva_wallet',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'GL' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'GG' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'VA' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'HU' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'viva_wallet',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'IS' => array(
+ 'primary_psp' => 'mollie',
+ 'primary_apm' => 'paypal_wallet',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'IE' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'primary_offline' => 'square',
+ 'other_psp' => array(
+ 'stripe',
+ 'square',
+ 'mollie',
+ 'visa_as',
+ 'airwallex',
+ 'viva_wallet',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'IM' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'IT' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'airwallex',
+ 'viva_wallet',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'JE' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'LV' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ ),
+ ),
+ 'LI' => array(
+ 'primary_psp' => 'stripe',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'mollie',
+ 'visa_as',
+ ),
+ ),
+ 'LT' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'airwallex',
+ ),
+ ),
+ 'LU' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'viva_wallet',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ ),
+ 'MT' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'viva_wallet',
+ ),
+ ),
+ 'MD' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'MC' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_wallet',
+ ),
+ 'ME' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'NL' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'viva_wallet',
+ 'klarna_checkout',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'MK' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'NO' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'klarna_checkout',
+ 'nexi_checkout',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'PL' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'airwallex',
+ 'viva_wallet',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'PT' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'airwallex',
+ 'viva_wallet',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'RO' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'viva_wallet',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'RU' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'SM' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'RS' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_wallet',
+ ),
+ 'SK' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'SI' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ ),
+ ),
+ 'ES' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'primary_offline' => 'square',
+ 'other_psp' => array(
+ 'stripe',
+ 'square',
+ 'mollie',
+ 'visa_as',
+ 'monei',
+ 'airwallex',
+ 'viva_wallet',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'SJ' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'SE' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ 'viva_wallet',
+ 'klarna_checkout',
+ 'nexi_checkout',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ ),
+ 'CH' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mollie',
+ 'visa_as',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ ),
+ 'TR' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'UA' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'AG' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'AI' => array(
+ 'primary_psp' => 'tilopay',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'AR' => array(
+ 'primary_psp' => 'mercado_pago',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'AW' => array(
+ 'primary_psp' => 'tilopay',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'BS' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'BB' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'BZ' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'BM' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'BO' => array(
+ 'primary_psp' => 'visa_as',
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'BQ' => array(
+ 'primary_psp' => 'tilopay',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'BR' => array(
+ 'primary_psp' => 'stripe',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'mercado_pago',
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'VG' => array(
+ 'primary_psp' => 'tilopay',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'KY' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'CL' => array(
+ 'primary_psp' => 'mercado_pago',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'CO' => array(
+ 'primary_psp' => 'mercado_pago',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'CR' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'CU' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'CW' => array(
+ 'primary_psp' => 'tilopay',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'DM' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'DO' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'EC' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'SV' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'FK' => array(
+ 'primary_psp' => 'visa_as',
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'GF' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'GD' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'GP' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'GT' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'GY' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'HT' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'HN' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'JM' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'MQ' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'MX' => array(
+ 'primary_psp' => 'stripe',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'mercado_pago',
+ 'visa_as',
+ ),
+ 'other_bnpl' => array(
+ 'klarna',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'MS' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'NI' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'PA' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'PY' => array(
+ 'primary_psp' => 'visa_as',
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'PE' => array(
+ 'primary_psp' => 'mercado_pago',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'PR' => array(
+ 'primary_psp' => 'visa_as',
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'BL' => array(
+ 'primary_psp' => 'visa_as',
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'KN' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'LC' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'MF' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'VC' => array(
+ 'primary_psp' => 'tilopay',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'SX' => array(
+ 'primary_psp' => 'tilopay',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'GS' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'SR' => array(
+ 'primary_psp' => 'tilopay',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'TT' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'TC' => array(
+ 'primary_psp' => 'tilopay',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'UY' => array(
+ 'primary_psp' => 'mercado_pago',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'VI' => array(
+ 'primary_psp' => 'tilopay',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'VE' => array(
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_crypto' => array(
+ 'heliopay',
+ ),
+ ),
+ 'AQ' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'AS' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'AU' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'primary_offline' => 'square',
+ 'other_psp' => array(
+ 'stripe',
+ 'square',
+ 'eway',
+ 'visa_as',
+ 'airwallex',
+ 'gocardless',
+ 'antom',
+ ),
+ 'other_bnpl' => array(
+ 'afterpay',
+ 'klarna',
+ ),
+ ),
+ 'BD' => array(
+ 'primary_psp' => 'payoneer',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'IO' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'BN' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'KH' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'CN' => array(
+ 'primary_psp' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'antom',
+ 'airwallex',
+ 'payoneer',
+ 'visa_as',
+ ),
+ ),
+ 'CX' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'CC' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'CK' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'FJ' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'GU' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'HM' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'HK' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'antom',
+ 'airwallex',
+ 'payoneer',
+ 'visa_as',
+ ),
+ ),
+ 'IN' => array(
+ 'primary_psp' => 'stripe',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'razorpay',
+ 'payu_india',
+ 'payoneer',
+ 'visa_as',
+ ),
+ ),
+ 'ID' => array(
+ 'primary_psp' => 'payoneer',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'JP' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'primary_offline' => 'square',
+ 'other_psp' => array(
+ 'stripe',
+ 'square',
+ 'komoju',
+ 'airwallex',
+ 'visa_as',
+ ),
+ 'other_express_checkout' => array(
+ 'amazon_pay',
+ ),
+ ),
+ 'KI' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'LA' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'MO' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'MY' => array(
+ 'primary_psp' => 'stripe',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'payoneer',
+ 'visa_as',
+ 'airwallex',
+ ),
+ ),
+ 'MV' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'MH' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'FM' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'MN' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'MM' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'NR' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'NP' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'NC' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'NZ' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'eway',
+ 'visa_as',
+ 'airwallex',
+ ),
+ 'other_bnpl' => array(
+ 'afterpay',
+ 'klarna',
+ ),
+ ),
+ 'NU' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'NF' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'MP' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'PW' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'PG' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'PH' => array(
+ 'primary_psp' => 'payoneer',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'PN' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'WS' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'SG' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'antom',
+ 'airwallex',
+ 'visa_as',
+ ),
+ ),
+ 'SB' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'LK' => array(
+ 'primary_psp' => 'payoneer',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'KR' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'airwallex',
+ ),
+ ),
+ 'TW' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_wallet',
+ ),
+ 'TH' => array(
+ 'primary_psp' => 'stripe',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'payoneer',
+ 'visa_as',
+ ),
+ ),
+ 'TL' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'TK' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'TO' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'TV' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'VU' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'VN' => array(
+ 'primary_psp' => 'payoneer',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'WF' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'DZ' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'AO' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'BJ' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'BW' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'BF' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'BI' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'CV' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'CM' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'CF' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'TD' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'KM' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'CG' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'CI' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'EG' => array(
+ 'primary_psp' => 'mastercard',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'paymob',
+ 'visa_as',
+ ),
+ ),
+ 'CD' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'DJ' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'GQ' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'ER' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'SZ' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'ET' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'TF' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'GA' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'GH' => array(
+ 'primary_psp' => 'paystack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'GM' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'GN' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'GW' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'KE' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'LS' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'LR' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'LY' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'MG' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'MW' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'ML' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'MR' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'MU' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'YT' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'MA' => array(
+ 'primary_psp' => 'payoneer',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'MZ' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'NA' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'NE' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'NG' => array(
+ 'primary_psp' => 'mastercard',
+ 'other_psp' => array(
+ 'paystack',
+ 'visa_as',
+ ),
+ ),
+ 'RE' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'RW' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'SH' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'ST' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'SN' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'SC' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'SL' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'SO' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'ZA' => array(
+ 'primary_psp' => 'mastercard',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'paystack',
+ 'payfast',
+ 'visa_as',
+ ),
+ ),
+ 'SS' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'TZ' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'TG' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'TN' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'UG' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'EH' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'ZM' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'ZW' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'AF' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'AZ' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'BH' => array(
+ 'primary_psp' => 'mastercard',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'BT' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'GE' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'IR' => array(),
+ 'IQ' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'IL' => array(
+ 'primary_psp' => 'airwallex',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'JO' => array(
+ 'primary_psp' => 'mastercard',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ 'ngenius',
+ ),
+ ),
+ 'KZ' => array(
+ 'primary_psp' => 'visa_as',
+ 'primary_apm' => 'paypal_full_stack',
+ ),
+ 'KW' => array(
+ 'primary_psp' => 'mastercard',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'KG' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'LB' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'OM' => array(
+ 'primary_psp' => 'paymob',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'PK' => array(
+ 'primary_psp' => 'mastercard',
+ 'other_psp' => array(
+ 'payoneer',
+ 'paymob',
+ 'visa_as',
+ ),
+ ),
+ 'PS' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'QA' => array(
+ 'primary_psp' => 'mastercard',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'visa_as',
+ ),
+ ),
+ 'SA' => array(
+ 'primary_psp' => 'mastercard',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'paymob',
+ 'visa_as',
+ 'ngenius',
+ ),
+ ),
+ 'SD' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'TJ' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'TM' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'AE' => array(
+ 'primary_psp' => 'woopayments',
+ 'primary_apm' => 'paypal_full_stack',
+ 'other_psp' => array(
+ 'stripe',
+ 'mastercard',
+ 'payoneer',
+ 'paymob',
+ 'visa_as',
+ 'ngenius',
+ ),
+ ),
+ 'UZ' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+ 'YE' => array(
+ 'primary_psp' => 'visa_as',
+ ),
+);