Commit ceaa8837ecc for woocommerce
commit ceaa8837ecc0554ac9da1319cae5d78aaea3e0f8
Author: Vlad Olaru <vlad.olaru@automattic.com>
Date: Fri Jul 31 10:10:40 2026 +0300
[Payments NOX] Ignore test-mode WooPayments usage in the WooPayments incentives eligibility context (#67154)
* fix: exclude test-mode usage from the WooPayments incentives context
The WooPayments suggestion incentive's has_wcpay() check treated any
WooPayments account cached on the store, and any order paid via
WooPayments, as proof the store had WooPayments in use. Neither check
distinguished live mode from test mode, and the result was frozen
forever in the
woocommerce_admin_pes_incentive_woopayments_store_had_woopayments
option. Trialing WooPayments with a test-drive account therefore
permanently disqualified the store from the Switch and Action
Incentives - the test-drive account alone was enough, before any order
was even placed.
Require live mode on both signals. The account cache check now skips
test-drive and sandbox accounts, mirroring how WooPayments itself
identifies a genuine live merchant in
WC_Payments_Account::maybe_record_kyc_completion_date(). The order
query is filtered to live-mode orders via the _wcpay_mode order meta
that WooPayments stamps at checkout.
Persist the version of the logic that determined the value alongside
the value itself, and re-determine a stored positive that predates the
current version. Without it the fix would only apply to stores that
had never had the value determined, leaving the affected stores
disqualified forever. A stored negative is trusted regardless of
version, since each revision of this logic is stricter than the last
and so cannot turn a negative into a positive.
That versioning is also what makes this safe to ship independently of
the WooPayments plugin. The plugin's WC_Payments_Incentives_Service
writes this same option from its own copy of the logic, which receives
the matching fix in Automattic/woocommerce-payments#12010. The plugin
only writes the option when it is absent and otherwise consumes it as
found, so whichever side ships first, core re-determines a value the
older logic left behind instead of the two racing to freeze it.
Orders carrying no _wcpay_mode meta no longer match. That covers
orders predating the meta (added 2022-04-25, first shipped in
WooPayments 4.1.0) and orders created outside the checkout flow that
saves it, such as WooPayments Subscriptions renewals. Worst case such
a store becomes incentive-eligible despite having used WooPayments,
which is far less harmful than the permanent false-ineligibility this
fixes.
Refs WOOPMNT-6320
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: describe the caching consideration for derived incentive context values
The incentives providers cache the store-context values they derive, and
the caching mechanism is picked per value with nothing recording why. The
WooPayments provider's has_wcpay() caches its value in an option, which
froze a value produced by logic that was later corrected, leaving those
stores with the wrong answer indefinitely.
Note the consideration on the Incentive base class, where a new provider
author will see it: a value cached beyond a single request outlives the
logic that produced it, unless the cache expires or the value records
which logic version produced it. Describe what has_wcpay() does about it,
including that its skip-re-deriving-a-cached-no shortcut rests on the
logic only ever narrowing what qualifies, so it doesn't carry over to
logic that widens what qualifies or to values richer than yes/no.
Name the cached values concretely ('no' and 'yes') in has_wcpay() too,
since the option holding the value and the one holding the logic version
sit next to each other and talking about positives and negatives invites
confusing the two.
Describing rather than prescribing, and documentation rather than a
shared mechanism: the other two context values are either uncached or
transient-backed and self-healing, so there is no second case yet to
generalize from.
Refs WOOPMNT-6320
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: roll back the DB transaction in the WooPayments incentive tests
WooPaymentsTest::tearDown() overrides the teardown without calling the
parent, and the parent is what runs the WordPress test framework's
tear_down() - the polyfill bridges tearDown() to it. That teardown issues
the ROLLBACK ending the per-test DB transaction, resets the query and post
globals, and clears the current user.
None of that ran for this class. Each setUp() creates an administrator
user, and those accumulated instead of being rolled back: probing the
suite showed 17 administrators by the last of 49 tests, against 2 once the
parent call is in place. The next test's START TRANSACTION implicitly
commits whatever the previous test left behind, so the rows outlive the
run.
Call parent::tearDown() last, so the class's own cleanup still runs inside
the transaction being rolled back.
The explicit option deletes stay. They are redundant against the rollback
for the DB rows, but they keep the intent visible and cover the in-memory
option cache.
Refs WOOPMNT-6320
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/fix-woopayments-incentive-ignore-test-mode-orders b/plugins/woocommerce/changelog/fix-woopayments-incentive-ignore-test-mode-orders
new file mode 100644
index 00000000000..69a1e1758db
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-woopayments-incentive-ignore-test-mode-orders
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Stop counting test-mode WooPayments usage - a test-drive account or test-mode orders - when determining WooPayments incentives eligibility, so trialing WooPayments no longer disqualifies a store from incentives. Stores already disqualified this way get their eligibility re-determined once.
diff --git a/plugins/woocommerce/src/Internal/Admin/Suggestions/Incentives/Incentive.php b/plugins/woocommerce/src/Internal/Admin/Suggestions/Incentives/Incentive.php
index 2e6981a3491..055b8866be4 100644
--- a/plugins/woocommerce/src/Internal/Admin/Suggestions/Incentives/Incentive.php
+++ b/plugins/woocommerce/src/Internal/Admin/Suggestions/Incentives/Incentive.php
@@ -5,6 +5,22 @@ namespace Automattic\WooCommerce\Internal\Admin\Suggestions\Incentives;
/**
* Abstract class for payment extension suggestion incentive provider classes.
+ *
+ * Caching derived store-context values
+ *
+ * Providers cache the store-context values they derive, since deriving them costs DB
+ * queries. A value cached beyond a single request outlives the logic that derived it, so
+ * correcting that logic later doesn't reach the stores already holding a value - unless
+ * the cache expires, or the value records which version of the logic produced it.
+ *
+ * Transients get this from their expiration. `WooPayments::has_wcpay()` caches in an
+ * option instead, and pairs it with a second option holding the logic version, re-deriving
+ * a value an older version produced. Its logic only ever narrows what qualifies, so it
+ * skips re-deriving a cached `no` - a shortcut that doesn't carry over to logic that
+ * widens what qualifies, or to a value richer than yes/no.
+ *
+ * That pairing also lets the WooPayments plugin, which writes the same value option from
+ * its own copy of the logic, ship before or after core.
*/
abstract class Incentive {
const PREFIX = 'woocommerce_admin_pes_incentive_';
diff --git a/plugins/woocommerce/src/Internal/Admin/Suggestions/Incentives/WooPayments.php b/plugins/woocommerce/src/Internal/Admin/Suggestions/Incentives/WooPayments.php
index a8cf3a48e90..13bfbe504f9 100644
--- a/plugins/woocommerce/src/Internal/Admin/Suggestions/Incentives/WooPayments.php
+++ b/plugins/woocommerce/src/Internal/Admin/Suggestions/Incentives/WooPayments.php
@@ -15,6 +15,21 @@ use WC_Abstract_Order;
* @internal
*/
class WooPayments extends Incentive {
+ /**
+ * The version of the logic used to determine if the store had WooPayments in use.
+ *
+ * The determined value is stored long-term (see `has_wcpay()`), and the WooPayments plugin
+ * stores its own determination under the very same option name. Persisting the version
+ * alongside the value lets us tell a value determined by the current logic from one
+ * determined by an earlier (or by the plugin's, potentially older) logic, so a stale
+ * positive gets re-determined instead of being trusted forever.
+ *
+ * Bump this whenever the determination logic gets stricter.
+ *
+ * @var int
+ */
+ private const STORE_HAD_WOOPAYMENTS_LOGIC_VERSION = 2;
+
/**
* The transient name for incentives cache.
*
@@ -36,6 +51,13 @@ class WooPayments extends Incentive {
*/
protected string $store_had_woopayments_option_name;
+ /**
+ * The option name used to store the logic version that determined the store had WooPayments value.
+ *
+ * @var string
+ */
+ protected string $store_had_woopayments_version_option_name;
+
/**
* The memoized incentives to avoid fetching multiple times during a request.
*
@@ -54,6 +76,8 @@ class WooPayments extends Incentive {
$this->cache_transient_name = self::PREFIX . $suggestion_id . '_cache';
$this->store_has_orders_transient_name = self::PREFIX . $suggestion_id . '_store_has_orders';
$this->store_had_woopayments_option_name = self::PREFIX . $suggestion_id . '_store_had_woopayments';
+
+ $this->store_had_woopayments_version_option_name = $this->store_had_woopayments_option_name . '_version';
}
/**
@@ -227,7 +251,7 @@ class WooPayments extends Incentive {
/**
* Check if the WooPayments payment gateway is active and set up or was at some point,
- * or there are orders processed with it, at some moment.
+ * or there are live-mode orders processed with it, at some moment.
*
* @return boolean Whether the store has WooPayments.
*/
@@ -238,21 +262,45 @@ class WooPayments extends Incentive {
// Since the past can't be changed, neither can this value.
$had_wcpay = get_option( $this->store_had_woopayments_option_name );
if ( false !== $had_wcpay ) {
- return filter_var( $had_wcpay, FILTER_VALIDATE_BOOLEAN );
+ $stored_value = filter_var( $had_wcpay, FILTER_VALIDATE_BOOLEAN );
+
+ // A cached 'no' holds whatever version produced it, since each revision of this logic
+ // only narrows what qualifies: a store that didn't qualify before can't start now.
+ //
+ // A cached 'yes' is trusted only when the current version produced it. Earlier
+ // revisions counted test-mode usage - a test-drive account, a test-mode order - as
+ // the real thing and froze that, so those get re-derived once. The WooPayments
+ // plugin writes this same option from its own copy of this logic, which may still
+ // be an older one, so this is what lets the two ship in either order instead of
+ // whichever runs first winning permanently.
+ if ( ! $stored_value
+ || (int) get_option( $this->store_had_woopayments_version_option_name, 0 ) >= self::STORE_HAD_WOOPAYMENTS_LOGIC_VERSION ) {
+
+ return $stored_value;
+ }
}
// We need to determine the value.
// Start with the assumption that the store didn't have WooPayments in use.
$had_wcpay = false;
- // We consider the store to have WooPayments if there is meaningful account data in the WooPayments account cache.
- // This implies that WooPayments was active at some point and that it was connected.
- // If WooPayments is active right now, we will not get to this point since the plugin is active check is done first.
+ // We consider the store to have WooPayments if there is meaningful live account data
+ // in the WooPayments account cache.
+ // This implies that WooPayments was active at some point and that it was connected with a live account.
if ( $this->has_wcpay_account_data() ) {
$had_wcpay = true;
}
- // If there is at least one order processed with WooPayments, we consider the store to have WooPayments.
+ // If there is at least one live-mode order processed with WooPayments, we consider the store to have WooPayments.
+ // Test-mode orders (e.g. placed while trialing WooPayments with a test-drive account) don't count
+ // since they don't represent real usage of WooPayments.
+ //
+ // Orders carrying no order mode meta at all don't count either, since the meta is what tells the two
+ // apart. That covers orders placed before WooPayments started saving it (April 2022, WooPayments 4.1.0)
+ // and orders created outside the checkout flow that saves it, like WooPayments Subscriptions renewals.
+ // Such a store may end up considered incentive-eligible despite having used WooPayments, which is the
+ // far less harmful direction to err in than permanently withholding incentives from a store that only
+ // ever tried WooPayments out.
if ( false === $had_wcpay && ! empty(
wc_get_orders(
array(
@@ -260,30 +308,55 @@ class WooPayments extends Incentive {
'return' => 'ids',
'limit' => 1,
'orderby' => 'none',
+ // Use the order mode meta saved by WooPayments to only count live-mode orders.
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+ 'meta_key' => '_wcpay_mode',
+ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
+ 'meta_value' => 'prod',
)
)
) ) {
$had_wcpay = true;
}
- // Store the value for future use.
+ // Store the value, and the logic version that determined it, for future use.
update_option( $this->store_had_woopayments_option_name, $had_wcpay ? 'yes' : 'no' );
+ update_option( $this->store_had_woopayments_version_option_name, self::STORE_HAD_WOOPAYMENTS_LOGIC_VERSION );
return $had_wcpay;
}
/**
- * Check if there is meaningful data in the WooPayments account cache.
+ * Check if there is meaningful live account data in the WooPayments account cache.
+ *
+ * Sandbox accounts don't count: a test-drive account is a trial of WooPayments,
+ * not actual use of it. This mirrors how WooPayments itself decides whether an
+ * account belongs to a genuine live merchant (see `WC_Payments_Account::maybe_record_kyc_completion_date()`).
*
* @return boolean
*/
private function has_wcpay_account_data(): bool {
$account_data = get_option( 'wcpay_account_data', array() );
- if ( ! empty( $account_data['data']['account_id'] ) ) {
- return true;
+ $account = $account_data['data'] ?? array();
+
+ if ( empty( $account['account_id'] ) ) {
+ return false;
}
- return false;
+ // A test-drive account is a trial of WooPayments, not real usage of it.
+ if ( ! empty( $account['is_test_drive'] ) ) {
+ return false;
+ }
+
+ // Sandbox accounts don't count either.
+ // Both flags are only acted upon when present: cached account data written by
+ // older WooPayments versions may not carry them, and we'd rather keep counting
+ // those stores as WooPayments users than reclassify them on missing data.
+ if ( isset( $account['is_live'] ) && ! $account['is_live'] ) {
+ return false;
+ }
+
+ return true;
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/Incentives/WooPaymentsTest.php b/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/Incentives/WooPaymentsTest.php
index af695a0888d..25e57ec3df1 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/Incentives/WooPaymentsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Admin/Suggestions/Incentives/WooPaymentsTest.php
@@ -13,6 +13,20 @@ use WC_Unit_Test_Case;
* @class WooPayments
*/
class WooPaymentsTest extends WC_Unit_Test_Case {
+ /**
+ * The option storing whether the store had WooPayments in use.
+ *
+ * @var string
+ */
+ private const HAD_WOOPAYMENTS_OPTION = 'woocommerce_admin_pes_incentive_suggestion1_store_had_woopayments';
+
+ /**
+ * The option storing the logic version that determined the store had WooPayments value.
+ *
+ * @var string
+ */
+ private const HAD_WOOPAYMENTS_VERSION_OPTION = self::HAD_WOOPAYMENTS_OPTION . '_version';
+
/**
* The system under test.
*
@@ -122,7 +136,13 @@ class WooPaymentsTest extends WC_Unit_Test_Case {
public function tearDown(): void {
remove_all_filters( 'pre_http_request' );
+ delete_option( self::HAD_WOOPAYMENTS_OPTION );
+ delete_option( self::HAD_WOOPAYMENTS_VERSION_OPTION );
+ delete_option( 'wcpay_account_data' );
+
$this->sut->clear_cache();
+
+ parent::tearDown();
}
/**
@@ -271,4 +291,285 @@ class WooPaymentsTest extends WC_Unit_Test_Case {
// Clean up.
delete_option( 'wcpay_account_data' );
}
+
+ /**
+ * Test that test-mode WooPayments orders don't mark the store as having had WooPayments.
+ */
+ public function test_incentives_context_ignores_test_mode_woopayments_orders() {
+ // Arrange.
+ $this->sut
+ ->method( 'is_extension_active' )
+ ->willReturn( false );
+
+ add_filter( 'pre_http_request', $this->response_mock_ref, 10, 3 );
+
+ $order = \WC_Helper_Order::create_order();
+ $order->set_payment_method( 'woocommerce_payments' );
+ $order->update_meta_data( '_wcpay_mode', 'test' );
+ $order->save();
+
+ delete_option( self::HAD_WOOPAYMENTS_OPTION );
+
+ // Act.
+ $this->sut->is_visible( 'incentive1', 'US' );
+
+ // Assert.
+ $this->assertSame( 'no', get_option( self::HAD_WOOPAYMENTS_OPTION ) );
+
+ // Clean up.
+ $order->delete( true );
+ }
+
+ /**
+ * Test that live-mode WooPayments orders mark the store as having had WooPayments.
+ */
+ public function test_incentives_context_counts_live_mode_woopayments_orders() {
+ // Arrange.
+ $this->sut
+ ->method( 'is_extension_active' )
+ ->willReturn( false );
+
+ add_filter( 'pre_http_request', $this->response_mock_ref, 10, 3 );
+
+ $order = \WC_Helper_Order::create_order();
+ $order->set_payment_method( 'woocommerce_payments' );
+ $order->update_meta_data( '_wcpay_mode', 'prod' );
+ $order->save();
+
+ delete_option( self::HAD_WOOPAYMENTS_OPTION );
+
+ // Act.
+ $this->sut->is_visible( 'incentive1', 'US' );
+
+ // Assert.
+ $this->assertSame( 'yes', get_option( self::HAD_WOOPAYMENTS_OPTION ) );
+
+ // Clean up.
+ $order->delete( true );
+ }
+
+ /**
+ * Test that WooPayments orders without the order mode meta don't mark the store as having had WooPayments.
+ *
+ * Such orders predate WooPayments saving the meta, or were created outside the checkout flow
+ * that saves it. Erring towards incentive eligibility is intentional.
+ */
+ public function test_incentives_context_ignores_woopayments_orders_without_mode_meta() {
+ // Arrange.
+ $this->sut
+ ->method( 'is_extension_active' )
+ ->willReturn( false );
+
+ add_filter( 'pre_http_request', $this->response_mock_ref, 10, 3 );
+
+ $order = \WC_Helper_Order::create_order();
+ $order->set_payment_method( 'woocommerce_payments' );
+ $order->save();
+
+ delete_option( self::HAD_WOOPAYMENTS_OPTION );
+
+ // Act.
+ $this->sut->is_visible( 'incentive1', 'US' );
+
+ // Assert.
+ $this->assertSame( 'no', get_option( self::HAD_WOOPAYMENTS_OPTION ) );
+
+ // Clean up.
+ $order->delete( true );
+ }
+
+ /**
+ * Test that test-drive account data doesn't mark the store as having had WooPayments.
+ */
+ public function test_incentives_context_ignores_test_drive_account_data() {
+ // Arrange.
+ $this->sut
+ ->method( 'is_extension_active' )
+ ->willReturn( false );
+
+ add_filter( 'pre_http_request', $this->response_mock_ref, 10, 3 );
+
+ update_option(
+ 'wcpay_account_data',
+ array(
+ 'data' => array(
+ 'account_id' => '123',
+ 'is_live' => false,
+ 'is_test_drive' => true,
+ ),
+ )
+ );
+
+ delete_option( self::HAD_WOOPAYMENTS_OPTION );
+
+ // Act.
+ $this->sut->is_visible( 'incentive1', 'US' );
+
+ // Assert.
+ $this->assertSame( 'no', get_option( self::HAD_WOOPAYMENTS_OPTION ) );
+ }
+
+ /**
+ * Test that sandbox account data doesn't mark the store as having had WooPayments.
+ */
+ public function test_incentives_context_ignores_sandbox_account_data() {
+ // Arrange.
+ $this->sut
+ ->method( 'is_extension_active' )
+ ->willReturn( false );
+
+ add_filter( 'pre_http_request', $this->response_mock_ref, 10, 3 );
+
+ update_option(
+ 'wcpay_account_data',
+ array(
+ 'data' => array(
+ 'account_id' => '123',
+ 'is_live' => false,
+ ),
+ )
+ );
+
+ delete_option( self::HAD_WOOPAYMENTS_OPTION );
+
+ // Act.
+ $this->sut->is_visible( 'incentive1', 'US' );
+
+ // Assert.
+ $this->assertSame( 'no', get_option( self::HAD_WOOPAYMENTS_OPTION ) );
+ }
+
+ /**
+ * Test that live account data marks the store as having had WooPayments.
+ */
+ public function test_incentives_context_counts_live_account_data() {
+ // Arrange.
+ $this->sut
+ ->method( 'is_extension_active' )
+ ->willReturn( false );
+
+ add_filter( 'pre_http_request', $this->response_mock_ref, 10, 3 );
+
+ update_option(
+ 'wcpay_account_data',
+ array(
+ 'data' => array(
+ 'account_id' => '123',
+ 'is_live' => true,
+ 'is_test_drive' => false,
+ ),
+ )
+ );
+
+ delete_option( self::HAD_WOOPAYMENTS_OPTION );
+
+ // Act.
+ $this->sut->is_visible( 'incentive1', 'US' );
+
+ // Assert.
+ $this->assertSame( 'yes', get_option( self::HAD_WOOPAYMENTS_OPTION ) );
+ }
+
+ /**
+ * Test that account data written before WooPayments saved the mode flags still counts.
+ */
+ public function test_incentives_context_counts_account_data_without_mode_flags() {
+ // Arrange.
+ $this->sut
+ ->method( 'is_extension_active' )
+ ->willReturn( false );
+
+ add_filter( 'pre_http_request', $this->response_mock_ref, 10, 3 );
+
+ update_option( 'wcpay_account_data', array( 'data' => array( 'account_id' => '123' ) ) );
+
+ delete_option( self::HAD_WOOPAYMENTS_OPTION );
+
+ // Act.
+ $this->sut->is_visible( 'incentive1', 'US' );
+
+ // Assert.
+ $this->assertSame( 'yes', get_option( self::HAD_WOOPAYMENTS_OPTION ) );
+ }
+
+ /**
+ * Test that a stored positive determined by an earlier logic version is re-determined.
+ *
+ * This is what keeps a value frozen by an earlier revision of this logic - or by an older
+ * WooPayments version writing the same option - from disqualifying the store forever.
+ */
+ public function test_incentives_context_redetermines_stale_positive() {
+ // Arrange.
+ $this->sut
+ ->method( 'is_extension_active' )
+ ->willReturn( false );
+
+ add_filter( 'pre_http_request', $this->response_mock_ref, 10, 3 );
+
+ // A positive stored without a logic version, as an earlier revision would have left it.
+ update_option( self::HAD_WOOPAYMENTS_OPTION, 'yes' );
+ delete_option( self::HAD_WOOPAYMENTS_VERSION_OPTION );
+
+ // Act.
+ $this->sut->is_visible( 'incentive1', 'US' );
+
+ // Assert.
+ $this->assertSame( 'no', get_option( self::HAD_WOOPAYMENTS_OPTION ) );
+ $this->assertSame( 2, (int) get_option( self::HAD_WOOPAYMENTS_VERSION_OPTION ) );
+ }
+
+ /**
+ * Test that a stored positive determined by the current logic version is trusted.
+ */
+ public function test_incentives_context_trusts_current_positive() {
+ // Arrange.
+ $this->sut
+ ->method( 'is_extension_active' )
+ ->willReturn( false );
+
+ add_filter( 'pre_http_request', $this->response_mock_ref, 10, 3 );
+
+ update_option( self::HAD_WOOPAYMENTS_OPTION, 'yes' );
+ update_option( self::HAD_WOOPAYMENTS_VERSION_OPTION, 2 );
+
+ // Act.
+ $this->sut->is_visible( 'incentive1', 'US' );
+
+ // Assert.
+ $this->assertSame( 'yes', get_option( self::HAD_WOOPAYMENTS_OPTION ) );
+ }
+
+ /**
+ * Test that a stored negative is trusted regardless of the logic version that determined it.
+ *
+ * Each revision of the logic is stricter than the one before it, so a store that didn't
+ * qualify under an earlier revision can't start qualifying under the current one.
+ */
+ public function test_incentives_context_trusts_stale_negative() {
+ // Arrange.
+ $this->sut
+ ->method( 'is_extension_active' )
+ ->willReturn( false );
+
+ add_filter( 'pre_http_request', $this->response_mock_ref, 10, 3 );
+
+ // A live-mode order that would determine a positive if the stored value were re-determined.
+ $order = \WC_Helper_Order::create_order();
+ $order->set_payment_method( 'woocommerce_payments' );
+ $order->update_meta_data( '_wcpay_mode', 'prod' );
+ $order->save();
+
+ // A negative stored without a logic version, as an earlier revision would have left it.
+ update_option( self::HAD_WOOPAYMENTS_OPTION, 'no' );
+ delete_option( self::HAD_WOOPAYMENTS_VERSION_OPTION );
+
+ // Act.
+ $this->sut->is_visible( 'incentive1', 'US' );
+
+ // Assert.
+ $this->assertSame( 'no', get_option( self::HAD_WOOPAYMENTS_OPTION ) );
+
+ // Clean up.
+ $order->delete( true );
+ }
}