Commit cde534eb781 for woocommerce
commit cde534eb78131d66499f1f2c39ce71ccfdbe4b92
Author: Thomas Roberts <5656702+opr@users.noreply.github.com>
Date: Wed Aug 26 11:09:46 2026 +0100
Move Back in Stock Notifications activation from wp-config flag to a Features setting (#64490)
* Move BIS activation from wp-config flag to a Features setting
The Back in Stock Notifications alpha was gated behind a
WOOCOMMERCE_BIS_ALPHA_ENABLED wp-config constant, which means store
owners can only opt in by editing wp-config.php — a non-starter for
the wider alpha audience we want to reach. Move the gate to a normal
WooCommerce feature toggle so it shows up in the standard place
(WooCommerce → Settings → Advanced → Features) and merchants can flip
it on/off from wp-admin.
- Register a `customer_stock_notifications` feature in
FeaturesController, marked experimental + alpha in the description so
the feature card sits under the Experimental heading and the wording
is unambiguous about API/data stability.
- Replace the constant check in `WooCommerce::init_classes` with
`FeaturesUtil::feature_is_enabled( 'customer_stock_notifications' )`,
and drop the now-unused `Constants` import from class-woocommerce.php.
- Drop the schema gate in `StockNotificationsDataStore::get_database_schema`
so the BIS tables are always created at install/upgrade time.
Otherwise enabling the feature mid-life would leave a no-op feature
with no tables to write to until the next plugin upgrade. Empty tables
are cheap; the feature flag now governs UI/behavior only.
- Update the legacy test bootstrap to set
`woocommerce_feature_customer_stock_notifications_enabled = yes`
instead of defining the now-removed constant.
Stores currently relying on the constant will need to re-enable the
feature once via the Features screen after upgrading. Acceptable
because the audience is alpha-flagged users only and the upgrade
notice can mention this.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Drop alpha wording from BIS feature card
The "(alpha)" suffix and "Alpha — interfaces, data, and behavior may
change" caveat are coming off — present in the original draft but not
needed on the merchant-facing card.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: gate stock notifications on init instead of at plugin load
* fix: migrate BIS alpha constant opt-in to the feature toggle option
* fix: handle stock notifications feature toggle setup and teardown
* refactor: drop redundant skip_compatibility_checks from BIS feature def
* fix: harden BIS feature toggle hook callback and data store gating
* docs: tighten BIS feature toggle code comments
* refactor: make BIS feature option name a single source of truth
* docs: correct why the BIS data store gate re-checks the option
* fix: overwrite the seeded feature option in the BIS alpha migration
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Chris Lilitsas <1105590+xristos3490@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/bis-features-screen-toggle b/plugins/woocommerce/changelog/bis-features-screen-toggle
new file mode 100644
index 00000000000..e31bc92d67b
--- /dev/null
+++ b/plugins/woocommerce/changelog/bis-features-screen-toggle
@@ -0,0 +1,4 @@
+Significance: minor
+Type: dev
+
+Move Back in Stock Notifications activation from the WOOCOMMERCE_BIS_ALPHA_ENABLED wp-config constant to a normal feature toggle at WooCommerce → Settings → Advanced → Features → Experimental.
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index 86cfde7f0f9..2d9979b09f0 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -350,6 +350,9 @@ class WC_Install {
'11.1.0-1' => array(
'wc_update_11101_remove_deprecated_variation_gallery_option',
),
+ '11.2.0' => array(
+ 'wc_update_1120_migrate_stock_notifications_alpha_constant',
+ ),
);
/**
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index a6d71af5628..7cda07c2945 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -46,8 +46,6 @@ use Automattic\WooCommerce\Internal\Caches\ProductVersionStringInvalidator;
use Automattic\WooCommerce\Internal\Caches\OrdersVersionStringInvalidator;
use Automattic\WooCommerce\Internal\Caches\TaxRateVersionStringInvalidator;
use Automattic\WooCommerce\Internal\CustomerEmailVerification\CustomerEmailVerification;
-use Automattic\WooCommerce\Internal\StockNotifications\StockNotifications;
-use Automattic\Jetpack\Constants;
/**
* Main WooCommerce Class.
@@ -412,11 +410,6 @@ final class WooCommerce {
$container->get( CustomerEmailVerification::class );
$container->get( OrderLogsCleanupHelper::class );
- // Feature flags.
- if ( Constants::is_true( 'WOOCOMMERCE_BIS_ALPHA_ENABLED' ) ) {
- $container->get( StockNotifications::class );
- }
-
/**
* These classes have a register method for attaching hooks.
*/
@@ -440,6 +433,7 @@ final class WooCommerce {
$container->get( Automattic\WooCommerce\Internal\Orders\PointOfSaleEmailHandler::class )->register();
$container->get( Automattic\WooCommerce\Internal\POS\POSController::class )->register();
$container->get( Automattic\WooCommerce\Internal\ShopperLists\ShopperListsController::class )->register();
+ $container->get( Automattic\WooCommerce\Internal\StockNotifications\StockNotifications::class )->register();
$container->get( Automattic\WooCommerce\Internal\ScheduledSalePriceReconciler::class )->register();
$container->get( Automattic\WooCommerce\Internal\OrderWithdrawal\OrderWithdrawalController::class )->register();
diff --git a/plugins/woocommerce/includes/wc-update-functions.php b/plugins/woocommerce/includes/wc-update-functions.php
index 06b5fda6f14..3b8402817a6 100644
--- a/plugins/woocommerce/includes/wc-update-functions.php
+++ b/plugins/woocommerce/includes/wc-update-functions.php
@@ -18,6 +18,7 @@
defined( 'ABSPATH' ) || exit;
+use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Admin\Notes\Note;
use Automattic\WooCommerce\Admin\Notes\Notes;
use Automattic\WooCommerce\Database\Migrations\MigrationHelper;
@@ -37,6 +38,7 @@ use Automattic\WooCommerce\Internal\ProductAttributesLookup\DataRegenerator;
use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register as Download_Directories;
use Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Synchronize as Download_Directories_Sync;
+use Automattic\WooCommerce\Internal\StockNotifications\StockNotifications;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Internal\Utilities\FilesystemUtil;
use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
@@ -3636,3 +3638,28 @@ function wc_update_1110_flush_product_count_cache() {
( new \Automattic\WooCommerce\Caches\ProductCountCache() )->flush( 'product' );
}
}
+
+/**
+ * Migrate the Back in Stock Notifications alpha opt-in from the
+ * WOOCOMMERCE_BIS_ALPHA_ENABLED constant to the feature toggle.
+ *
+ * The option is written with update_option() rather than add_option() because
+ * WC_Install::create_options() runs first and has already seeded every Features
+ * screen checkbox with its default. No store can have chosen 'no' deliberately:
+ * the toggle does not exist before this release.
+ *
+ * Writing the option fires 'updated_option', which FeaturesController turns into
+ * FEATURE_ENABLED_CHANGED_ACTION, so the feature's own activation side effects
+ * (the data retention task and the rewrite rules flush) run from there.
+ *
+ * @since 11.2.0
+ *
+ * @return void
+ */
+function wc_update_1120_migrate_stock_notifications_alpha_constant() {
+ if ( ! Constants::is_true( 'WOOCOMMERCE_BIS_ALPHA_ENABLED' ) ) {
+ return;
+ }
+
+ update_option( StockNotifications::ENABLE_OPTION_NAME, 'yes', true );
+}
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index ed977d05612..0fb4f327e79 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -67500,12 +67500,6 @@ parameters:
count: 1
path: src/Internal/StockNotifications/Privacy/PrivacyEraser.php
- -
- message: '#^Method Automattic\\WooCommerce\\Internal\\StockNotifications\\StockNotifications\:\:init_hooks\(\) has no return type specified\.$#'
- identifier: missingType.return
- count: 1
- path: src/Internal/StockNotifications/StockNotifications.php
-
-
message: '#^Method Automattic\\WooCommerce\\Internal\\StockNotifications\\StockNotifications\:\:on_install_or_update\(\) has no return type specified\.$#'
identifier: missingType.return
diff --git a/plugins/woocommerce/src/Internal/DataStores/StockNotifications/StockNotificationsDataStore.php b/plugins/woocommerce/src/Internal/DataStores/StockNotifications/StockNotificationsDataStore.php
index 38b0ede624b..3d94d603c85 100644
--- a/plugins/woocommerce/src/Internal/DataStores/StockNotifications/StockNotificationsDataStore.php
+++ b/plugins/woocommerce/src/Internal/DataStores/StockNotifications/StockNotificationsDataStore.php
@@ -7,7 +7,6 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\DataStores\StockNotifications;
-use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Internal\StockNotifications\Notification;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Internal\StockNotifications\Enums\NotificationStatus;
@@ -73,11 +72,6 @@ class StockNotificationsDataStore implements \WC_Object_Data_Store_Interface {
* @return string
*/
public function get_database_schema(): string {
-
- if ( ! Constants::is_true( 'WOOCOMMERCE_BIS_ALPHA_ENABLED' ) ) {
- return '';
- }
-
global $wpdb;
$collate = $wpdb->has_cap( 'collation' ) ? $wpdb->get_charset_collate() : '';
diff --git a/plugins/woocommerce/src/Internal/Features/FeaturesController.php b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
index d61a80d585b..e067fb01c69 100644
--- a/plugins/woocommerce/src/Internal/Features/FeaturesController.php
+++ b/plugins/woocommerce/src/Internal/Features/FeaturesController.php
@@ -18,6 +18,7 @@ use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableControlle
use Automattic\WooCommerce\Internal\CostOfGoodsSold\CostOfGoodsSoldController;
use Automattic\WooCommerce\Internal\ProductGallery\ProductMediaGallery;
use Automattic\WooCommerce\Internal\PushNotifications\PushNotifications;
+use Automattic\WooCommerce\Internal\StockNotifications\StockNotifications;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\PluginUtil;
@@ -709,6 +710,18 @@ class FeaturesController {
'is_experimental' => true,
'disable_ui' => false,
),
+ StockNotifications::FEATURE_NAME => array(
+ 'name' => __( 'Back in Stock Notifications', 'woocommerce' ),
+ 'description' => __(
+ 'Allow customers to sign up to receive an email when an out-of-stock product is back in stock.',
+ 'woocommerce'
+ ),
+ 'option_key' => StockNotifications::ENABLE_OPTION_NAME,
+ 'enabled_by_default' => false,
+ 'is_experimental' => true,
+ 'disable_ui' => false,
+ 'default_plugin_compatibility' => FeaturePluginCompatibility::COMPATIBLE,
+ ),
BlockEditorUnifiedAssets::FEATURE_NAME => array(
'name' => __( 'Unified block editor assets', 'woocommerce' ),
'description' => __( 'Load WooCommerce block editor scripts and styles from shared bundles to improve editor performance.', 'woocommerce' ),
diff --git a/plugins/woocommerce/src/Internal/StockNotifications/StockNotifications.php b/plugins/woocommerce/src/Internal/StockNotifications/StockNotifications.php
index 8129ab721ec..f2c6162a563 100644
--- a/plugins/woocommerce/src/Internal/StockNotifications/StockNotifications.php
+++ b/plugins/woocommerce/src/Internal/StockNotifications/StockNotifications.php
@@ -5,6 +5,8 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\StockNotifications;
use Automattic\WooCommerce\Internal\DataStores\StockNotifications\StockNotificationsDataStore;
+use Automattic\WooCommerce\Internal\Features\FeaturesController;
+use Automattic\WooCommerce\Internal\RegisterHooksInterface;
use Automattic\WooCommerce\Internal\StockNotifications\Emails\EmailActionController;
use Automattic\WooCommerce\Internal\StockNotifications\StockSyncController;
use Automattic\WooCommerce\Internal\StockNotifications\Privacy\PrivacyEraser;
@@ -14,36 +16,98 @@ use Automattic\WooCommerce\Internal\StockNotifications\Admin\AdminManager;
use Automattic\WooCommerce\Internal\StockNotifications\Frontend\ProductPageIntegration;
use Automattic\WooCommerce\Internal\StockNotifications\Frontend\FormHandlerService;
use Automattic\WooCommerce\Internal\StockNotifications\Frontend\NotificationManagementService;
+use Automattic\WooCommerce\Utilities\FeaturesUtil;
/**
* The controller for the stock notifications.
*/
-class StockNotifications {
+class StockNotifications implements RegisterHooksInterface {
/**
- * Initialize the controller.
+ * The feature that gates the whole Back in Stock Notifications experience.
*/
- public function __construct() {
- add_action( 'plugins_loaded', array( $this, 'init_hooks' ) );
+ public const FEATURE_NAME = 'customer_stock_notifications';
+
+ /**
+ * The option that stores the feature's on/off state.
+ *
+ * Declared as `option_key` in the feature definition, so this constant is the
+ * single source of truth rather than a copy of a derived name.
+ */
+ public const ENABLE_OPTION_NAME = 'woocommerce_feature_customer_stock_notifications_enabled';
+
+ /**
+ * Register the hooks that must exist regardless of the feature's state.
+ *
+ * The services are wired up in `maybe_init_services()`, behind the feature check.
+ * The feature-change listener cannot live there: during the request that turns the
+ * feature on it is still off, so it would never observe its own activation.
+ *
+ * @internal
+ */
+ public function register(): void {
+ add_action( 'init', array( $this, 'maybe_init_services' ), 1 );
add_action( 'woocommerce_installed', array( $this, 'on_install_or_update' ) );
+ add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'on_feature_enabled_changed' ), 10, 2 );
}
/**
- * Handle the WooCommerce installation event.
- *
- * This method is called when WooCommerce is installed or updated.
- * It initializes the data retention controller to set up necessary tasks.
+ * Set up the data retention tasks when WooCommerce is installed or updated.
*/
public function on_install_or_update() {
+ if ( ! FeaturesUtil::feature_is_enabled( self::FEATURE_NAME ) ) {
+ return;
+ }
+
wc_get_container()->get( DataRetentionController::class )->on_woo_install_or_update();
}
/**
- * Register hooks and services.
+ * Set up or tear down the feature's side effects when it is toggled.
+ *
+ * Toggling from the Features screen fires no install event, so the daily data
+ * retention task and the rewrite rules have to be handled here.
+ *
+ * Params are untyped and coerced in the body because any third-party code can
+ * fire this action with fewer or differently-typed arguments.
+ *
+ * @param mixed $feature_id The feature that changed.
+ * @param mixed $enabled Whether the feature is now enabled.
+ *
+ * @internal
+ */
+ public function on_feature_enabled_changed( $feature_id, $enabled = false ): void {
+ if ( self::FEATURE_NAME !== $feature_id ) {
+ return;
+ }
+
+ $enabled = filter_var( $enabled, FILTER_VALIDATE_BOOLEAN );
+
+ // The My Account endpoint appears or disappears with the feature.
+ update_option( 'woocommerce_queue_flush_rewrite_rules', 'yes' );
+
+ $data_retention_controller = wc_get_container()->get( DataRetentionController::class );
+
+ if ( $enabled ) {
+ $data_retention_controller->on_woo_install_or_update();
+ } else {
+ $data_retention_controller->clear_daily_task();
+ }
+ }
+
+ /**
+ * Wire up the services, unless the feature is disabled.
+ *
+ * Runs on `init` priority 1: after the textdomain is loaded, and before
+ * `WC_Install::check_version()` (priority 5) fires `woocommerce_installed`.
*
* @internal
*/
- public function init_hooks() {
+ public function maybe_init_services(): void {
+ if ( ! FeaturesUtil::feature_is_enabled( self::FEATURE_NAME ) ) {
+ return;
+ }
+
add_filter( 'woocommerce_data_stores', array( $this, 'register_data_stores' ) );
$container = wc_get_container();
@@ -64,12 +128,20 @@ class StockNotifications {
}
/**
- * Register the data stores.
+ * Register the data stores, unless the feature is disabled.
*
* @param array $data_stores Data stores.
* @return array
*/
public function register_data_stores( $data_stores ) {
+ // WC_Data_Store::__construct() re-applies this filter on every data store load,
+ // so re-check the option: the feature can be switched off after this callback was
+ // attached at `init`. Read the option directly rather than through
+ // feature_is_enabled(), which builds translated feature definitions.
+ if ( 'yes' !== get_option( self::ENABLE_OPTION_NAME, 'no' ) ) {
+ return $data_stores;
+ }
+
if ( ! is_array( $data_stores ) ) {
return $data_stores;
}
diff --git a/plugins/woocommerce/tests/legacy/bootstrap.php b/plugins/woocommerce/tests/legacy/bootstrap.php
index faa162bfbff..83113959cd7 100644
--- a/plugins/woocommerce/tests/legacy/bootstrap.php
+++ b/plugins/woocommerce/tests/legacy/bootstrap.php
@@ -260,12 +260,6 @@ class WC_Unit_Tests_Bootstrap {
define( 'WC_TAX_ROUNDING_MODE', 'auto' );
define( 'WC_USE_TRANSACTIONS', false );
- // Default Back In Stock alpha to enabled during tests when no
- // per-suite override has been set.
- if ( ! defined( 'WOOCOMMERCE_BIS_ALPHA_ENABLED' ) ) {
- define( 'WOOCOMMERCE_BIS_ALPHA_ENABLED', true );
- }
-
update_option( 'woocommerce_enable_coupons', 'yes' );
update_option( 'woocommerce_calc_taxes', 'yes' );
update_option( 'woocommerce_onboarding_opt_in', 'yes' );
@@ -300,6 +294,11 @@ class WC_Unit_Tests_Bootstrap {
// set in time for ProductCacheController::on_init() to register its invalidation hooks.
update_option( 'woocommerce_feature_product_instance_caching_enabled', 'yes' );
+ // Enable Back In Stock Notifications during tests. Set here rather than in load_wc()
+ // because install_wc() includes uninstall.php, which deletes every 'woocommerce_%'
+ // option. This still runs on `setup_theme`, in time for the `init` feature check.
+ update_option( 'woocommerce_feature_customer_stock_notifications_enabled', 'yes' );
+
// Reload capabilities after install, see https://core.trac.wordpress.org/ticket/28374.
if ( version_compare( $GLOBALS['wp_version'], '4.7', '<' ) ) {
$GLOBALS['wp_roles']->reinit();
diff --git a/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php b/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
index 70455142608..f70b26bc70f 100644
--- a/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
+++ b/plugins/woocommerce/tests/php/includes/wc-update-functions-test.php
@@ -5,8 +5,10 @@
* @package WooCommerce\Tests\Functions.
*/
+use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Blocks\Options as BlockOptions;
use Automattic\WooCommerce\Blocks\Utils\BlockTemplateUtils;
+use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\VariationGallery\Package as VariationGalleryPackage;
/**
@@ -14,6 +16,15 @@ use Automattic\WooCommerce\Internal\VariationGallery\Package as VariationGallery
*/
class WC_Update_Functions_Test extends \WC_Unit_Test_Case {
+ /**
+ * Tear down test fixtures.
+ */
+ public function tearDown(): void {
+ Constants::clear_single_constant( 'WOOCOMMERCE_BIS_ALPHA_ENABLED' );
+ delete_option( 'woocommerce_feature_customer_stock_notifications_enabled' );
+ parent::tearDown();
+ }
+
/**
* Test wc_update_343_cleanup_foreign_keys() function.
*/
@@ -379,4 +390,82 @@ class WC_Update_Functions_Test extends \WC_Unit_Test_Case {
wc_update_1110_delete_dashboard_outofstock_count_transient();
$this->assertFalse( get_transient( 'wc_outofstock_count' ) );
}
+
+ /**
+ * @testdox Migration enables the customer_stock_notifications feature when the alpha constant is set.
+ */
+ public function test_wc_update_1120_migrate_stock_notifications_alpha_constant_opts_in(): void {
+ include_once WC_ABSPATH . 'includes/wc-update-functions.php';
+
+ $db_updates = WC_Install::get_db_update_callbacks();
+ $this->assertArrayHasKey( '11.2.0', $db_updates );
+ $this->assertContains( 'wc_update_1120_migrate_stock_notifications_alpha_constant', $db_updates['11.2.0'] );
+
+ delete_option( 'woocommerce_feature_customer_stock_notifications_enabled' );
+ Constants::set_constant( 'WOOCOMMERCE_BIS_ALPHA_ENABLED', true );
+
+ wc_update_1120_migrate_stock_notifications_alpha_constant();
+
+ $this->assertSame( 'yes', get_option( 'woocommerce_feature_customer_stock_notifications_enabled' ) );
+ }
+
+ /**
+ * @testdox Migration leaves the feature untouched when the alpha constant is absent or falsy.
+ */
+ public function test_wc_update_1120_migrate_stock_notifications_alpha_constant_without_opt_in(): void {
+ include_once WC_ABSPATH . 'includes/wc-update-functions.php';
+
+ delete_option( 'woocommerce_feature_customer_stock_notifications_enabled' );
+ Constants::clear_single_constant( 'WOOCOMMERCE_BIS_ALPHA_ENABLED' );
+
+ wc_update_1120_migrate_stock_notifications_alpha_constant();
+
+ $this->assertFalse( get_option( 'woocommerce_feature_customer_stock_notifications_enabled' ) );
+
+ Constants::set_constant( 'WOOCOMMERCE_BIS_ALPHA_ENABLED', false );
+
+ wc_update_1120_migrate_stock_notifications_alpha_constant();
+
+ $this->assertFalse( get_option( 'woocommerce_feature_customer_stock_notifications_enabled' ) );
+ }
+
+ /**
+ * @testdox Migration overwrites the 'no' that WC_Install::create_options() seeds before the update callbacks run.
+ */
+ public function test_wc_update_1120_migrate_stock_notifications_alpha_constant_overwrites_seeded_option(): void {
+ include_once WC_ABSPATH . 'includes/wc-update-functions.php';
+
+ update_option( 'woocommerce_feature_customer_stock_notifications_enabled', 'no' );
+ Constants::set_constant( 'WOOCOMMERCE_BIS_ALPHA_ENABLED', true );
+
+ wc_update_1120_migrate_stock_notifications_alpha_constant();
+
+ $this->assertSame( 'yes', get_option( 'woocommerce_feature_customer_stock_notifications_enabled' ) );
+ }
+
+ /**
+ * @testdox Migration lets FeaturesController announce the change, so the feature runs its own activation side effects.
+ */
+ public function test_wc_update_1120_migrate_stock_notifications_alpha_constant_fires_feature_enabled_changed(): void {
+ include_once WC_ABSPATH . 'includes/wc-update-functions.php';
+
+ update_option( 'woocommerce_feature_customer_stock_notifications_enabled', 'no' );
+ Constants::set_constant( 'WOOCOMMERCE_BIS_ALPHA_ENABLED', true );
+
+ $changes = array();
+ $listener = function ( $feature_id, $enabled ) use ( &$changes ) {
+ $changes[ $feature_id ] = $enabled;
+ };
+
+ add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, $listener, 10, 2 );
+
+ try {
+ wc_update_1120_migrate_stock_notifications_alpha_constant();
+ } finally {
+ remove_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, $listener, 10 );
+ }
+
+ $this->assertArrayHasKey( 'customer_stock_notifications', $changes );
+ $this->assertTrue( $changes['customer_stock_notifications'] );
+ }
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/StockNotifications/StockNotificationsTests.php b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/StockNotificationsTests.php
new file mode 100644
index 00000000000..b4793cfcba5
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/StockNotifications/StockNotificationsTests.php
@@ -0,0 +1,89 @@
+<?php
+
+declare( strict_types = 1 );
+namespace Automattic\WooCommerce\Tests\Internal\StockNotifications;
+
+use Automattic\WooCommerce\Internal\Features\FeaturesController;
+use Automattic\WooCommerce\Internal\StockNotifications\DataRetentionController;
+use Automattic\WooCommerce\Internal\StockNotifications\StockNotifications;
+
+/**
+ * StockNotifications controller tests.
+ */
+class StockNotificationsTests extends \WC_Unit_Test_Case {
+
+ /**
+ * Clean up after tests.
+ */
+ public function tearDown(): void {
+ wc_get_container()->get( DataRetentionController::class )->clear_daily_task();
+ delete_option( 'woocommerce_customer_stock_notifications_unverified_deletions_days_threshold' );
+ delete_option( 'woocommerce_queue_flush_rewrite_rules' );
+ parent::tearDown();
+ }
+
+ /**
+ * Fire the feature-changed action the way FeaturesController does.
+ *
+ * @param bool $enabled Whether the feature is now enabled.
+ * @param string $feature_id The feature that changed.
+ */
+ private function fire_feature_changed( bool $enabled, string $feature_id = StockNotifications::FEATURE_NAME ): void {
+ do_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, $feature_id, $enabled );
+ }
+
+ /**
+ * @testdox Enabling the feature schedules the daily data retention task.
+ */
+ public function test_enabling_the_feature_schedules_the_daily_task(): void {
+ update_option( 'woocommerce_customer_stock_notifications_unverified_deletions_days_threshold', 30 );
+ wc_get_container()->get( DataRetentionController::class )->clear_daily_task();
+ $this->assertFalse( wp_get_schedule( DataRetentionController::DAILY_TASK_HOOK ) );
+
+ $this->fire_feature_changed( true );
+
+ $this->assertSame( 'daily', wp_get_schedule( DataRetentionController::DAILY_TASK_HOOK ) );
+ }
+
+ /**
+ * @testdox Disabling the feature clears the daily data retention task.
+ */
+ public function test_disabling_the_feature_clears_the_daily_task(): void {
+ update_option( 'woocommerce_customer_stock_notifications_unverified_deletions_days_threshold', 30 );
+ $this->assertSame( 'daily', wp_get_schedule( DataRetentionController::DAILY_TASK_HOOK ) );
+
+ $this->fire_feature_changed( false );
+
+ $this->assertFalse( wp_get_schedule( DataRetentionController::DAILY_TASK_HOOK ) );
+ }
+
+ /**
+ * @testdox Toggling the feature queues a rewrite rules flush for the My Account endpoint.
+ */
+ public function test_toggling_the_feature_queues_a_rewrite_flush(): void {
+ foreach ( array( true, false ) as $enabled ) {
+ delete_option( 'woocommerce_queue_flush_rewrite_rules' );
+
+ $this->fire_feature_changed( $enabled );
+
+ $this->assertSame(
+ 'yes',
+ get_option( 'woocommerce_queue_flush_rewrite_rules' ),
+ 'Toggling the feature should queue a rewrite rules flush.'
+ );
+ }
+ }
+
+ /**
+ * @testdox Changes to unrelated features are ignored.
+ */
+ public function test_other_features_are_ignored(): void {
+ update_option( 'woocommerce_customer_stock_notifications_unverified_deletions_days_threshold', 30 );
+ delete_option( 'woocommerce_queue_flush_rewrite_rules' );
+
+ $this->fire_feature_changed( false, 'some_other_feature' );
+
+ $this->assertSame( 'daily', wp_get_schedule( DataRetentionController::DAILY_TASK_HOOK ) );
+ $this->assertFalse( get_option( 'woocommerce_queue_flush_rewrite_rules' ) );
+ }
+}