Commit 62be524ca5 for woocommerce
commit 62be524ca57fa37c98e5c0d328ab021de5eda85c
Author: Néstor Soriano <konamiman@konamiman.com>
Date: Thu Feb 5 10:30:36 2026 +0100
Add the OrdersVersionStringInvalidator class (#63079)
diff --git a/plugins/woocommerce/changelog/pr-63079 b/plugins/woocommerce/changelog/pr-63079
new file mode 100644
index 0000000000..3573138e0a
--- /dev/null
+++ b/plugins/woocommerce/changelog/pr-63079
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Add the OrdersVersionStringInvalidator class
diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index 8efb7e9b3e..c81a67655e 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -35,6 +35,7 @@ use Automattic\WooCommerce\Utilities\{LoggingUtil, TimeUtil};
use Automattic\WooCommerce\Internal\Logging\RemoteLogger;
use Automattic\WooCommerce\Caches\OrderCountCacheService;
use Automattic\WooCommerce\Internal\Caches\ProductVersionStringInvalidator;
+use Automattic\WooCommerce\Internal\Caches\OrdersVersionStringInvalidator;
use Automattic\WooCommerce\Internal\Caches\TaxRateVersionStringInvalidator;
use Automattic\WooCommerce\Internal\StockNotifications\StockNotifications;
use Automattic\Jetpack\Constants;
@@ -361,6 +362,7 @@ final class WooCommerce {
$container->get( AbilitiesRegistry::class );
$container->get( MCPAdapterProvider::class );
$container->get( ProductVersionStringInvalidator::class );
+ $container->get( OrdersVersionStringInvalidator::class );
$container->get( TaxRateVersionStringInvalidator::class );
// Feature flags.
diff --git a/plugins/woocommerce/src/Internal/Caches/OrdersVersionStringInvalidator.php b/plugins/woocommerce/src/Internal/Caches/OrdersVersionStringInvalidator.php
new file mode 100644
index 0000000000..f269f8337a
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/Caches/OrdersVersionStringInvalidator.php
@@ -0,0 +1,346 @@
+<?php
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Caches;
+
+/**
+ * Order version string invalidation handler.
+ *
+ * This class provides an 'invalidate' method that will invalidate
+ * the version string for a given order, which in turn invalidates
+ * any cached REST API responses containing that order.
+ */
+class OrdersVersionStringInvalidator {
+
+ /**
+ * Stores the customer ID of orders before they are saved.
+ * Used to detect customer changes that require list invalidation.
+ *
+ * @var array<int, int> Order ID => Customer ID
+ */
+ private array $pre_save_customer_ids = array();
+
+ /**
+ * Initialize the invalidator and register hooks.
+ *
+ * Hooks are only registered when both conditions are met:
+ * - The REST API caching feature is enabled
+ * - The backend caching setting is active
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ *
+ * @internal
+ */
+ final public function init(): void {
+ // We can't use FeaturesController::feature_is_enabled at this point
+ // (before the 'init' action is triggered) because that would cause
+ // "Translation loading for the woocommerce domain was triggered too early" warnings.
+ if ( 'yes' !== get_option( 'woocommerce_feature_rest_api_caching_enabled' ) ) {
+ return;
+ }
+
+ if ( 'yes' === get_option( 'woocommerce_rest_api_enable_backend_caching', 'no' ) ) {
+ $this->register_hooks();
+ }
+ }
+
+ /**
+ * Register all order-related hooks.
+ *
+ * Only WooCommerce hooks are registered (not WordPress post hooks) since these always fire
+ * when an order is created/updated/deleted via the WooCommerce APIs, regardless of HPOS
+ * being active or not.
+ *
+ * @return void
+ */
+ private function register_hooks(): void {
+ // Hook to capture customer ID before save for change detection.
+ add_action( 'woocommerce_before_order_object_save', array( $this, 'handle_before_order_save' ), 10, 1 );
+
+ // WooCommerce CRUD hooks for orders.
+ add_action( 'woocommerce_new_order', array( $this, 'handle_woocommerce_new_order' ), 10, 2 );
+ add_action( 'woocommerce_update_order', array( $this, 'handle_woocommerce_update_order' ), 10, 2 );
+ add_action( 'woocommerce_before_delete_order', array( $this, 'handle_woocommerce_before_delete_order' ), 10, 2 );
+ add_action( 'woocommerce_trash_order', array( $this, 'handle_woocommerce_trash_order' ), 10, 1 );
+ add_action( 'woocommerce_untrash_order', array( $this, 'handle_woocommerce_untrash_order' ), 10, 2 );
+
+ // Status change hook.
+ add_action( 'woocommerce_order_status_changed', array( $this, 'handle_woocommerce_order_status_changed' ), 10, 4 );
+
+ // Refund hooks.
+ add_action( 'woocommerce_order_refunded', array( $this, 'handle_woocommerce_order_refunded' ), 10, 2 );
+ add_action( 'woocommerce_refund_deleted', array( $this, 'handle_woocommerce_refund_deleted' ), 10, 2 );
+ }
+
+ // phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+
+ /**
+ * Handle the woocommerce_before_order_object_save hook.
+ *
+ * Captures the customer ID before save to detect changes.
+ *
+ * @param \WC_Order $order The order being saved.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ *
+ * @internal
+ */
+ public function handle_before_order_save( $order ): void {
+ if ( ! $order instanceof \WC_Order || 'shop_order' !== $order->get_type() ) {
+ return;
+ }
+
+ $order_id = $order->get_id();
+ if ( $order_id > 0 ) {
+ $this->pre_save_customer_ids[ $order_id ] = (int) $order->get_data()['customer_id'];
+ }
+ }
+
+ /**
+ * Handle the woocommerce_new_order hook.
+ *
+ * @param int $order_id The order ID.
+ * @param \WC_Order $order The order object.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ *
+ * @internal
+ */
+ public function handle_woocommerce_new_order( $order_id, $order ): void {
+ $this->invalidate( (int) $order_id );
+ $this->invalidate_orders_list();
+ }
+
+ /**
+ * Handle the woocommerce_update_order hook.
+ *
+ * @param int $order_id The order ID.
+ * @param \WC_Order $order The order object.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ *
+ * @internal
+ */
+ public function handle_woocommerce_update_order( $order_id, $order ): void {
+ $order_id = (int) $order_id;
+ $this->invalidate( $order_id );
+
+ if ( $this->did_customer_change( $order_id, $order ) ) {
+ $this->invalidate_orders_list();
+ }
+
+ unset( $this->pre_save_customer_ids[ $order_id ] );
+ }
+
+ /**
+ * Check if the customer ID changed during the update.
+ *
+ * @param int $order_id The order ID.
+ * @param \WC_Order $order The order object (after save).
+ *
+ * @return bool True if customer changed.
+ */
+ private function did_customer_change( int $order_id, $order ): bool {
+ if ( ! isset( $this->pre_save_customer_ids[ $order_id ] ) ) {
+ return false;
+ }
+
+ $old_customer_id = $this->pre_save_customer_ids[ $order_id ];
+ $new_customer_id = $order instanceof \WC_Order ? (int) $order->get_customer_id() : 0;
+
+ return $old_customer_id !== $new_customer_id;
+ }
+
+ /**
+ * Handle the woocommerce_before_delete_order hook.
+ *
+ * @param int $order_id The order ID.
+ * @param \WC_Order $order The order object.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ *
+ * @internal
+ */
+ public function handle_woocommerce_before_delete_order( $order_id, $order ): void {
+ $this->invalidate( (int) $order_id );
+ $this->invalidate_orders_list();
+ }
+
+ /**
+ * Handle the woocommerce_trash_order hook.
+ *
+ * @param int $order_id The order ID.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ *
+ * @internal
+ */
+ public function handle_woocommerce_trash_order( $order_id ): void {
+ $this->invalidate( (int) $order_id );
+ $this->invalidate_orders_list();
+ }
+
+ /**
+ * Handle the woocommerce_untrash_order hook.
+ *
+ * @param int $order_id The order ID.
+ * @param string $previous_status The previous order status before trashing.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ *
+ * @internal
+ */
+ public function handle_woocommerce_untrash_order( $order_id, $previous_status ): void {
+ $this->invalidate( (int) $order_id );
+ $this->invalidate_orders_list();
+ }
+
+ /**
+ * Handle the woocommerce_order_status_changed hook.
+ *
+ * Status changes affect which orders appear in status-filtered collection endpoints,
+ * so we always invalidate the orders list.
+ *
+ * @param int $order_id The order ID.
+ * @param string $from_status The old status.
+ * @param string $to_status The new status.
+ * @param \WC_Order $order The order object.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ *
+ * @internal
+ */
+ public function handle_woocommerce_order_status_changed( $order_id, $from_status, $to_status, $order ): void {
+ $this->invalidate( (int) $order_id );
+ $this->invalidate_orders_list();
+ }
+
+ /**
+ * Handle the woocommerce_order_refunded hook.
+ *
+ * @param int $order_id The parent order ID.
+ * @param int $refund_id The refund ID.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ *
+ * @internal
+ */
+ public function handle_woocommerce_order_refunded( $order_id, $refund_id ): void {
+ $order_id = (int) $order_id;
+ $refund_id = (int) $refund_id;
+
+ $this->invalidate( $order_id );
+ $this->invalidate_refund( $refund_id );
+ $this->invalidate_order_refunds_list( $order_id );
+ $this->invalidate_refunds_list();
+ }
+
+ /**
+ * Handle the woocommerce_refund_deleted hook.
+ *
+ * @param int $refund_id The refund ID.
+ * @param int $order_id The parent order ID.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ *
+ * @internal
+ */
+ public function handle_woocommerce_refund_deleted( $refund_id, $order_id ): void {
+ $order_id = (int) $order_id;
+ $refund_id = (int) $refund_id;
+
+ $this->invalidate( $order_id );
+ $this->invalidate_refund( $refund_id );
+ $this->invalidate_order_refunds_list( $order_id );
+ $this->invalidate_refunds_list();
+ }
+
+ // phpcs:enable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
+
+ /**
+ * Invalidate an order version string.
+ *
+ * @param int $order_id The order ID.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ */
+ public function invalidate( int $order_id ): void {
+ wc_get_container()->get( VersionStringGenerator::class )->delete_version( "order_{$order_id}" );
+ }
+
+ /**
+ * Invalidate a refund version string.
+ *
+ * @param int $refund_id The refund ID.
+ *
+ * @return void
+ *
+ * @since 10.6.0
+ */
+ public function invalidate_refund( int $refund_id ): void {
+ wc_get_container()->get( VersionStringGenerator::class )->delete_version( "refund_{$refund_id}" );
+ }
+
+ /**
+ * Invalidate the orders list version string.
+ *
+ * This should be called when orders are created, deleted, change status,
+ * or change customer, as these operations affect collection/list endpoints.
+ *
+ * @return void
+ */
+ private function invalidate_orders_list(): void {
+ wc_get_container()->get( VersionStringGenerator::class )->delete_version( 'list_orders' );
+ }
+
+ /**
+ * Invalidate the refunds list version string.
+ *
+ * This should be called when refunds are created or deleted,
+ * as these operations affect the /refunds collection endpoint.
+ *
+ * @return void
+ */
+ private function invalidate_refunds_list(): void {
+ wc_get_container()->get( VersionStringGenerator::class )->delete_version( 'list_refunds' );
+ }
+
+ /**
+ * Invalidate the refunds list version string for a specific order.
+ *
+ * This should be called when refunds are created or deleted for an order,
+ * as these operations affect the /orders/{id}/refunds collection endpoint.
+ *
+ * @param int $order_id The parent order ID.
+ *
+ * @return void
+ */
+ private function invalidate_order_refunds_list( int $order_id ): void {
+ if ( $order_id > 0 ) {
+ wc_get_container()->get( VersionStringGenerator::class )->delete_version( "list_order_refunds_{$order_id}" );
+ }
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Caches/OrdersVersionStringInvalidatorTest.php b/plugins/woocommerce/tests/php/src/Internal/Caches/OrdersVersionStringInvalidatorTest.php
new file mode 100644
index 0000000000..7244ba90fd
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/Caches/OrdersVersionStringInvalidatorTest.php
@@ -0,0 +1,417 @@
+<?php
+/**
+ * OrdersVersionStringInvalidatorTest class file.
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\Caches;
+
+use Automattic\WooCommerce\Internal\Caches\OrdersVersionStringInvalidator;
+use Automattic\WooCommerce\Internal\Caches\VersionStringGenerator;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for the OrdersVersionStringInvalidator class.
+ */
+class OrdersVersionStringInvalidatorTest extends WC_Unit_Test_Case {
+
+ /**
+ * The System Under Test.
+ *
+ * @var OrdersVersionStringInvalidator
+ */
+ private $sut;
+
+ /**
+ * Version string generator.
+ *
+ * @var VersionStringGenerator
+ */
+ private $version_generator;
+
+ /**
+ * Set up test fixtures.
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ $this->sut = new OrdersVersionStringInvalidator();
+ $this->version_generator = wc_get_container()->get( VersionStringGenerator::class );
+ }
+
+ /**
+ * Tear down test fixtures.
+ */
+ public function tearDown(): void {
+ delete_option( 'woocommerce_feature_rest_api_caching_enabled' );
+ delete_option( 'woocommerce_rest_api_enable_backend_caching' );
+ parent::tearDown();
+ }
+
+ /**
+ * Enable the feature and backend caching, and initialize a new invalidator with hooks registered.
+ *
+ * @return OrdersVersionStringInvalidator The initialized invalidator.
+ */
+ private function get_invalidator_with_hooks_enabled(): OrdersVersionStringInvalidator {
+ update_option( 'woocommerce_feature_rest_api_caching_enabled', 'yes' );
+ update_option( 'woocommerce_rest_api_enable_backend_caching', 'yes' );
+
+ $invalidator = new OrdersVersionStringInvalidator();
+ $invalidator->init();
+
+ return $invalidator;
+ }
+
+ /**
+ * @testdox Invalidate method deletes the order version string from cache.
+ */
+ public function test_invalidate_deletes_version_string(): void {
+ $order_id = 123;
+
+ $this->version_generator->generate_version( "order_{$order_id}" );
+
+ $version_before = $this->version_generator->get_version( "order_{$order_id}", false );
+ $this->assertNotNull( $version_before, 'Version string should exist before invalidation' );
+
+ $this->sut->invalidate( $order_id );
+
+ $version_after = $this->version_generator->get_version( "order_{$order_id}", false );
+ $this->assertNull( $version_after, 'Version string should be deleted after invalidation' );
+ }
+
+ /**
+ * @testdox Invalidate_refund method deletes the refund version string from cache.
+ */
+ public function test_invalidate_refund_deletes_version_string(): void {
+ $refund_id = 456;
+
+ $this->version_generator->generate_version( "refund_{$refund_id}" );
+
+ $version_before = $this->version_generator->get_version( "refund_{$refund_id}", false );
+ $this->assertNotNull( $version_before, 'Version string should exist before invalidation' );
+
+ $this->sut->invalidate_refund( $refund_id );
+
+ $version_after = $this->version_generator->get_version( "refund_{$refund_id}", false );
+ $this->assertNull( $version_after, 'Version string should be deleted after invalidation' );
+ }
+
+ /**
+ * @testdox Hooks are registered when feature is enabled and backend caching is active.
+ */
+ public function test_hooks_registered_when_feature_and_setting_enabled(): void {
+ $invalidator = $this->get_invalidator_with_hooks_enabled();
+
+ $this->assertNotFalse( has_action( 'woocommerce_new_order', array( $invalidator, 'handle_woocommerce_new_order' ) ) );
+ $this->assertNotFalse( has_action( 'woocommerce_update_order', array( $invalidator, 'handle_woocommerce_update_order' ) ) );
+ $this->assertNotFalse( has_action( 'woocommerce_order_status_changed', array( $invalidator, 'handle_woocommerce_order_status_changed' ) ) );
+ $this->assertNotFalse( has_action( 'woocommerce_order_refunded', array( $invalidator, 'handle_woocommerce_order_refunded' ) ) );
+ }
+
+ /**
+ * @testdox Hooks are not registered when feature is disabled.
+ */
+ public function test_hooks_not_registered_when_feature_disabled(): void {
+ update_option( 'woocommerce_feature_rest_api_caching_enabled', 'no' );
+ update_option( 'woocommerce_rest_api_enable_backend_caching', 'yes' );
+
+ $invalidator = new OrdersVersionStringInvalidator();
+ $invalidator->init();
+
+ $this->assertFalse( has_action( 'woocommerce_new_order', array( $invalidator, 'handle_woocommerce_new_order' ) ) );
+ $this->assertFalse( has_action( 'woocommerce_update_order', array( $invalidator, 'handle_woocommerce_update_order' ) ) );
+ $this->assertFalse( has_action( 'woocommerce_order_status_changed', array( $invalidator, 'handle_woocommerce_order_status_changed' ) ) );
+ }
+
+ /**
+ * @testdox Hooks are not registered when backend caching setting is disabled.
+ */
+ public function test_hooks_not_registered_when_backend_caching_disabled(): void {
+ update_option( 'woocommerce_feature_rest_api_caching_enabled', 'yes' );
+ update_option( 'woocommerce_rest_api_enable_backend_caching', 'no' );
+
+ $invalidator = new OrdersVersionStringInvalidator();
+ $invalidator->init();
+
+ $this->assertFalse( has_action( 'woocommerce_new_order', array( $invalidator, 'handle_woocommerce_new_order' ) ) );
+ $this->assertFalse( has_action( 'woocommerce_update_order', array( $invalidator, 'handle_woocommerce_update_order' ) ) );
+ $this->assertFalse( has_action( 'woocommerce_order_status_changed', array( $invalidator, 'handle_woocommerce_order_status_changed' ) ) );
+ }
+
+ /**
+ * @testdox Creating a new order invalidates the order version string and list.
+ */
+ public function test_order_creation_invalidates_version_strings(): void {
+ $this->get_invalidator_with_hooks_enabled();
+
+ $this->version_generator->generate_version( 'list_orders' );
+ $list_version_before = $this->version_generator->get_version( 'list_orders', false );
+ $this->assertNotNull( $list_version_before, 'List version string should exist before order creation' );
+
+ $order = \WC_Helper_Order::create_order();
+ $order_id = $order->get_id();
+
+ $order_version = $this->version_generator->get_version( "order_{$order_id}", false );
+ $this->assertNull( $order_version, 'Order version string should be deleted after creation' );
+
+ $list_version_after = $this->version_generator->get_version( 'list_orders', false );
+ $this->assertNull( $list_version_after, 'List version string should be deleted after order creation' );
+ }
+
+ /**
+ * @testdox Updating an existing order invalidates the order version string.
+ */
+ public function test_order_update_invalidates_version_string(): void {
+ $this->get_invalidator_with_hooks_enabled();
+
+ $order = \WC_Helper_Order::create_order();
+ $order_id = $order->get_id();
+
+ $this->version_generator->generate_version( "order_{$order_id}" );
+ $version_before = $this->version_generator->get_version( "order_{$order_id}", false );
+ $this->assertNotNull( $version_before, 'Version string should exist before update' );
+
+ $order->set_billing_first_name( 'Updated Name' );
+ $order->save();
+
+ $version_after = $this->version_generator->get_version( "order_{$order_id}", false );
+ $this->assertNull( $version_after, 'Version string should be deleted after order update' );
+ }
+
+ /**
+ * @testdox Changing order customer invalidates the orders list.
+ */
+ public function test_order_customer_change_invalidates_list(): void {
+ $this->get_invalidator_with_hooks_enabled();
+
+ $order = \WC_Helper_Order::create_order( 1 );
+
+ $this->version_generator->generate_version( 'list_orders' );
+ $list_version_before = $this->version_generator->get_version( 'list_orders', false );
+ $this->assertNotNull( $list_version_before, 'List version string should exist before customer change' );
+
+ $order->set_customer_id( 2 );
+ $order->save();
+
+ $list_version_after = $this->version_generator->get_version( 'list_orders', false );
+ $this->assertNull( $list_version_after, 'List version string should be deleted after customer change' );
+ }
+
+ /**
+ * @testdox Updating order without customer change does not invalidate the list.
+ */
+ public function test_order_update_without_customer_change_does_not_invalidate_list(): void {
+ $this->get_invalidator_with_hooks_enabled();
+
+ $order = \WC_Helper_Order::create_order( 1 );
+
+ $this->version_generator->generate_version( 'list_orders' );
+ $list_version_before = $this->version_generator->get_version( 'list_orders', false );
+ $this->assertNotNull( $list_version_before, 'List version string should exist before update' );
+
+ $order->set_billing_first_name( 'Different Name' );
+ $order->save();
+
+ $list_version_after = $this->version_generator->get_version( 'list_orders', false );
+ $this->assertNotNull( $list_version_after, 'List version string should still exist after non-customer update' );
+ }
+
+ /**
+ * @testdox Changing order status invalidates the orders list.
+ */
+ public function test_order_status_change_invalidates_list(): void {
+ $this->get_invalidator_with_hooks_enabled();
+
+ $order = \WC_Helper_Order::create_order();
+ $order_id = $order->get_id();
+
+ $this->version_generator->generate_version( "order_{$order_id}" );
+ $this->version_generator->generate_version( 'list_orders' );
+
+ $order_version_before = $this->version_generator->get_version( "order_{$order_id}", false );
+ $list_version_before = $this->version_generator->get_version( 'list_orders', false );
+ $this->assertNotNull( $order_version_before, 'Order version string should exist before status change' );
+ $this->assertNotNull( $list_version_before, 'List version string should exist before status change' );
+
+ $order->set_status( 'completed' );
+ $order->save();
+
+ $order_version_after = $this->version_generator->get_version( "order_{$order_id}", false );
+ $list_version_after = $this->version_generator->get_version( 'list_orders', false );
+ $this->assertNull( $order_version_after, 'Order version string should be deleted after status change' );
+ $this->assertNull( $list_version_after, 'List version string should be deleted after status change' );
+ }
+
+ /**
+ * @testdox Trashing an order invalidates the order version string and list.
+ */
+ public function test_order_trash_invalidates_version_strings(): void {
+ $this->get_invalidator_with_hooks_enabled();
+
+ $order = \WC_Helper_Order::create_order();
+ $order_id = $order->get_id();
+
+ $this->version_generator->generate_version( "order_{$order_id}" );
+ $this->version_generator->generate_version( 'list_orders' );
+
+ $order_version_before = $this->version_generator->get_version( "order_{$order_id}", false );
+ $list_version_before = $this->version_generator->get_version( 'list_orders', false );
+ $this->assertNotNull( $order_version_before, 'Order version string should exist before trashing' );
+ $this->assertNotNull( $list_version_before, 'List version string should exist before trashing' );
+
+ $order->delete( false );
+
+ $order_version_after = $this->version_generator->get_version( "order_{$order_id}", false );
+ $list_version_after = $this->version_generator->get_version( 'list_orders', false );
+ $this->assertNull( $order_version_after, 'Order version string should be deleted after trashing' );
+ $this->assertNull( $list_version_after, 'List version string should be deleted after trashing' );
+ }
+
+ /**
+ * @testdox Deleting an order invalidates the order version string and list.
+ */
+ public function test_order_deletion_invalidates_version_strings(): void {
+ $this->get_invalidator_with_hooks_enabled();
+
+ $order = \WC_Helper_Order::create_order();
+ $order_id = $order->get_id();
+
+ $this->version_generator->generate_version( "order_{$order_id}" );
+ $this->version_generator->generate_version( 'list_orders' );
+
+ $order_version_before = $this->version_generator->get_version( "order_{$order_id}", false );
+ $list_version_before = $this->version_generator->get_version( 'list_orders', false );
+ $this->assertNotNull( $order_version_before, 'Order version string should exist before deletion' );
+ $this->assertNotNull( $list_version_before, 'List version string should exist before deletion' );
+
+ $order->delete( true );
+
+ $order_version_after = $this->version_generator->get_version( "order_{$order_id}", false );
+ $list_version_after = $this->version_generator->get_version( 'list_orders', false );
+ $this->assertNull( $order_version_after, 'Order version string should be deleted after deletion' );
+ $this->assertNull( $list_version_after, 'List version string should be deleted after deletion' );
+ }
+
+ /**
+ * @testdox Creating a refund invalidates the parent order and refund lists.
+ */
+ public function test_refund_creation_invalidates_version_strings(): void {
+ $this->get_invalidator_with_hooks_enabled();
+
+ $order = \WC_Helper_Order::create_order();
+ $order_id = $order->get_id();
+ $order->set_status( 'completed' );
+ $order->save();
+
+ $this->version_generator->generate_version( "order_{$order_id}" );
+ $this->version_generator->generate_version( 'list_refunds' );
+ $this->version_generator->generate_version( "list_order_refunds_{$order_id}" );
+
+ $order_version_before = $this->version_generator->get_version( "order_{$order_id}", false );
+ $refunds_list_version_before = $this->version_generator->get_version( 'list_refunds', false );
+ $order_refunds_version_before = $this->version_generator->get_version( "list_order_refunds_{$order_id}", false );
+ $this->assertNotNull( $order_version_before, 'Order version string should exist before refund' );
+ $this->assertNotNull( $refunds_list_version_before, 'Refunds list version string should exist before refund' );
+ $this->assertNotNull( $order_refunds_version_before, 'Order refunds list version string should exist before refund' );
+
+ $refund = wc_create_refund(
+ array(
+ 'order_id' => $order_id,
+ 'amount' => 1,
+ 'reason' => 'Test refund',
+ )
+ );
+
+ $refund_id = $refund->get_id();
+
+ $order_version_after = $this->version_generator->get_version( "order_{$order_id}", false );
+ $refund_version_after = $this->version_generator->get_version( "refund_{$refund_id}", false );
+ $refunds_list_version_after = $this->version_generator->get_version( 'list_refunds', false );
+ $order_refunds_version_after = $this->version_generator->get_version( "list_order_refunds_{$order_id}", false );
+
+ $this->assertNull( $order_version_after, 'Order version string should be deleted after refund' );
+ $this->assertNull( $refund_version_after, 'Refund version string should be deleted after creation' );
+ $this->assertNull( $refunds_list_version_after, 'Refunds list version string should be deleted after refund' );
+ $this->assertNull( $order_refunds_version_after, 'Order refunds list version string should be deleted after refund' );
+ }
+
+ /**
+ * @testdox Deleting a refund invalidates the parent order and refund lists.
+ */
+ public function test_refund_deletion_invalidates_version_strings(): void {
+ $this->get_invalidator_with_hooks_enabled();
+
+ $order = \WC_Helper_Order::create_order();
+ $order_id = $order->get_id();
+ $order->set_status( 'completed' );
+ $order->save();
+
+ $refund = wc_create_refund(
+ array(
+ 'order_id' => $order_id,
+ 'amount' => 1,
+ 'reason' => 'Test refund',
+ )
+ );
+ $refund_id = $refund->get_id();
+
+ $this->version_generator->generate_version( "order_{$order_id}" );
+ $this->version_generator->generate_version( "refund_{$refund_id}" );
+ $this->version_generator->generate_version( 'list_refunds' );
+ $this->version_generator->generate_version( "list_order_refunds_{$order_id}" );
+
+ $order_version_before = $this->version_generator->get_version( "order_{$order_id}", false );
+ $refund_version_before = $this->version_generator->get_version( "refund_{$refund_id}", false );
+ $refunds_list_version_before = $this->version_generator->get_version( 'list_refunds', false );
+ $order_refunds_version_before = $this->version_generator->get_version( "list_order_refunds_{$order_id}", false );
+ $this->assertNotNull( $order_version_before, 'Order version string should exist before refund deletion' );
+ $this->assertNotNull( $refund_version_before, 'Refund version string should exist before deletion' );
+ $this->assertNotNull( $refunds_list_version_before, 'Refunds list version string should exist before deletion' );
+ $this->assertNotNull( $order_refunds_version_before, 'Order refunds list version string should exist before deletion' );
+
+ $refund->delete( true );
+ // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- Test code.
+ do_action( 'woocommerce_refund_deleted', $refund_id, $order_id );
+
+ $order_version_after = $this->version_generator->get_version( "order_{$order_id}", false );
+ $refund_version_after = $this->version_generator->get_version( "refund_{$refund_id}", false );
+ $refunds_list_version_after = $this->version_generator->get_version( 'list_refunds', false );
+ $order_refunds_version_after = $this->version_generator->get_version( "list_order_refunds_{$order_id}", false );
+
+ $this->assertNull( $order_version_after, 'Order version string should be deleted after refund deletion' );
+ $this->assertNull( $refund_version_after, 'Refund version string should be deleted after deletion' );
+ $this->assertNull( $refunds_list_version_after, 'Refunds list version string should be deleted after refund deletion' );
+ $this->assertNull( $order_refunds_version_after, 'Order refunds list version string should be deleted after refund deletion' );
+ }
+
+ /**
+ * @testdox Handle methods can be called directly for manual invalidation.
+ */
+ public function test_handle_methods_can_be_called_directly(): void {
+ $order_id = 100;
+ $refund_id = 200;
+
+ $this->version_generator->generate_version( "order_{$order_id}" );
+ $this->version_generator->generate_version( "refund_{$refund_id}" );
+ $this->version_generator->generate_version( 'list_orders' );
+ $this->version_generator->generate_version( 'list_refunds' );
+
+ $mock_order = $this->createMock( \WC_Order::class );
+ $mock_order->method( 'get_id' )->willReturn( $order_id );
+ $mock_order->method( 'get_type' )->willReturn( 'shop_order' );
+ $mock_order->method( 'get_customer_id' )->willReturn( 1 );
+
+ $this->sut->handle_woocommerce_new_order( $order_id, $mock_order );
+
+ $this->assertNull(
+ $this->version_generator->get_version( "order_{$order_id}", false ),
+ 'Order version should be invalidated by handle_woocommerce_new_order'
+ );
+ $this->assertNull(
+ $this->version_generator->get_version( 'list_orders', false ),
+ 'Orders list version should be invalidated by handle_woocommerce_new_order'
+ );
+ }
+}