Commit e6299bc9220 for woocommerce

commit e6299bc9220ff2a53d63fdd6dd09a5863b971c33
Author: Jorge A. Torres <jorge.torres@automattic.com>
Date:   Fri Mar 27 16:24:21 2026 +0000

    Add periodic cleanup for order debug logs (#63756)

diff --git a/plugins/woocommerce/changelog/62143-cleanup-orphaned-order-debug-logs b/plugins/woocommerce/changelog/62143-cleanup-orphaned-order-debug-logs
new file mode 100644
index 00000000000..37ad8dcc01d
--- /dev/null
+++ b/plugins/woocommerce/changelog/62143-cleanup-orphaned-order-debug-logs
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Add periodic cleanup for place-order debug log files and dangling order meta that the existing batch processor doesn't cover.
diff --git a/plugins/woocommerce/includes/wc-core-functions.php b/plugins/woocommerce/includes/wc-core-functions.php
index 5ee994e06f8..9d732973e23 100644
--- a/plugins/woocommerce/includes/wc-core-functions.php
+++ b/plugins/woocommerce/includes/wc-core-functions.php
@@ -11,6 +11,7 @@
 use Automattic\Jetpack\Constants;
 use Automattic\WooCommerce\Utilities\NumberUtil;
 use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
+use Automattic\WooCommerce\Internal\Logging\OrderLogsCleanupHelper;

 if ( ! defined( 'ABSPATH' ) ) {
 	exit;
@@ -2025,6 +2026,8 @@ function wc_cleanup_logs() {
 	if ( is_callable( array( $logger, 'clear_expired_logs' ) ) ) {
 		$logger->clear_expired_logs();
 	}
+
+	wc_get_container()->get( OrderLogsCleanupHelper::class )->cleanup();
 }
 add_action( 'woocommerce_cleanup_logs', 'wc_cleanup_logs' );

diff --git a/plugins/woocommerce/src/Internal/Logging/OrderLogsCleanupHelper.php b/plugins/woocommerce/src/Internal/Logging/OrderLogsCleanupHelper.php
new file mode 100644
index 00000000000..c8cebd525fd
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/Logging/OrderLogsCleanupHelper.php
@@ -0,0 +1,248 @@
+<?php
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Logging;
+
+use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
+use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
+use WC_Logger;
+
+/**
+ * Handles cleanup of place-order debug log files and associated order meta.
+ *
+ * @since 10.7.0
+ */
+class OrderLogsCleanupHelper {
+
+	/**
+	 * Maximum number of log files to delete per run.
+	 */
+	public const MAX_FILES_PER_RUN = 100;
+
+	/**
+	 * Maximum number of orders to clean up per run.
+	 */
+	public const MAX_ORDERS_PER_RUN = 100;
+
+	/**
+	 * True if HPOS is enabled.
+	 *
+	 * @var bool
+	 */
+	private bool $hpos_in_use = false;
+
+	/**
+	 * True if HPOS is disabled and the orders data store in use is the old CPT one.
+	 *
+	 * @var bool
+	 */
+	private bool $cpt_in_use = false;
+
+	/**
+	 * The instance of DataSynchronizer to use.
+	 *
+	 * @var DataSynchronizer
+	 */
+	private DataSynchronizer $data_synchronizer;
+
+	/**
+	 * Initialize the instance.
+	 * This is invoked by the dependency injection container.
+	 *
+	 * @internal
+	 *
+	 * @param CustomOrdersTableController $hpos_controller The instance of CustomOrdersTableController to use.
+	 * @param DataSynchronizer            $data_synchronizer The instance of DataSynchronizer to use.
+	 *
+	 * @return void
+	 */
+	final public function init( CustomOrdersTableController $hpos_controller, DataSynchronizer $data_synchronizer ): void {
+		$this->hpos_in_use = $hpos_controller->custom_orders_table_usage_is_enabled();
+		if ( ! $this->hpos_in_use ) {
+			$this->cpt_in_use = \WC_Order_Data_Store_CPT::class === \WC_Data_Store::load( 'order' )->get_current_class_name();
+		}
+
+		$this->data_synchronizer = $data_synchronizer;
+	}
+
+	/**
+	 * Get the maximum age for debug logs before cleanup, in seconds.
+	 * Returns 0 if cleanup is disabled via filter.
+	 *
+	 * @return int
+	 */
+	private function get_max_age_in_seconds(): int {
+		/**
+		 * Filter the retention period for place-order debug logs cleanup.
+		 * Return 0 to disable cleanup entirely.
+		 *
+		 * @param int $max_age_in_seconds The maximum age in seconds before cleanup. Default 3 days.
+		 *
+		 * @since 10.7.0
+		 */
+		return absint( apply_filters( 'woocommerce_cleanup_order_debug_logs_max_age', 3 * DAY_IN_SECONDS ) );
+	}
+
+	/**
+	 * Run all cleanup tasks: dangling order meta and old log files.
+	 *
+	 * @since 10.7.0
+	 */
+	public function cleanup(): void {
+		$max_age = $this->get_max_age_in_seconds();
+
+		if ( 0 === $max_age ) {
+			return;
+		}
+
+		// Dangling orders have `_debug_log_source` meta but no `_debug_log_source_pending_deletion`.
+		$dangling_orders = $this->get_dangling_orders( $max_age );
+		$this->clear_logs_and_delete_meta( $dangling_orders );
+
+		// Old log files are those that are older than the given max age.
+		$this->cleanup_old_log_files( $max_age );
+	}
+
+	/**
+	 * Delete place-order-debug-* log files from the filesystem.
+	 *
+	 * @param int $max_age Maximum age in seconds before a file is eligible for deletion.
+	 */
+	private function cleanup_old_log_files( int $max_age ): void {
+		if ( \Automattic\WooCommerce\Utilities\LoggingUtil::get_default_handler() !== \Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2::class ) {
+			return;
+		}
+
+		$file_controller = wc_get_container()->get( \Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController::class );
+		$files           = $file_controller->get_files(
+			array(
+				'source'      => 'place-order-debug',
+				'date_filter' => 'modified',
+				'date_start'  => 1,
+				'date_end'    => time() - $max_age,
+				'per_page'    => self::MAX_FILES_PER_RUN,
+			)
+		);
+
+		if ( ! is_array( $files ) ) {
+			return;
+		}
+
+		foreach ( $files as $file ) {
+			$file->delete();
+		}
+	}
+
+	/**
+	 * Clear debug log files and delete associated order meta for the given items.
+	 * Deletes both `_debug_log_source` and `_debug_log_source_pending_deletion` meta.
+	 *
+	 * @since 10.7.0
+	 *
+	 * @param array $items Associative array of order ID => log source name.
+	 *
+	 * @return void
+	 */
+	public function clear_logs_and_delete_meta( array $items ): void {
+		if ( empty( $items ) ) {
+			return;
+		}
+
+		$logger = wc_get_logger();
+		if ( $logger instanceof WC_Logger ) {
+			foreach ( $items as $source ) {
+				$logger->clear( $source );
+			}
+		}
+
+		$order_ids = array_keys( $items );
+		$this->delete_debug_log_meta_entries( $order_ids );
+	}
+
+	/**
+	 * Get orders with `_debug_log_source` meta older than the given max age.
+	 *
+	 * Orders that also have `_debug_log_source_pending_deletion` will be handled
+	 * by the batch processor, but cleaning them up here too is harmless.
+	 *
+	 * @param int $max_age Maximum age in seconds.
+	 *
+	 * @return array Associative array of order ID => log source name.
+	 */
+	private function get_dangling_orders( int $max_age ): array {
+		if ( ! $this->hpos_in_use && ! $this->cpt_in_use ) {
+			return array();
+		}
+
+		global $wpdb;
+
+		$cutoff_date = gmdate( 'Y-m-d H:i:s', time() - $max_age );
+
+		$meta_table  = $this->hpos_in_use ? "{$wpdb->prefix}wc_orders_meta" : $wpdb->postmeta;
+		$order_table = $this->hpos_in_use ? "{$wpdb->prefix}wc_orders" : $wpdb->posts;
+		$id_column   = $this->hpos_in_use ? 'order_id' : 'post_id';
+		$type_column = $this->hpos_in_use ? 'type' : 'post_type';
+		$date_column = $this->hpos_in_use ? 'date_created_gmt' : 'post_date_gmt';
+
+		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+		$rows = $wpdb->get_results(
+			$wpdb->prepare(
+				"SELECT m.{$id_column} as order_id, m.meta_value
+				 FROM {$meta_table} m
+				 INNER JOIN {$order_table} o ON m.{$id_column} = o.id
+				 WHERE m.meta_key = %s
+				 AND o.{$type_column} = %s
+				 AND o.{$date_column} < %s
+				 LIMIT %d",
+				'_debug_log_source',
+				'shop_order',
+				$cutoff_date,
+				self::MAX_ORDERS_PER_RUN
+			),
+			ARRAY_A
+		);
+		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+
+		return array_column( $rows, 'meta_value', 'order_id' );
+	}
+
+	/**
+	 * Delete `_debug_log_source` and `_debug_log_source_pending_deletion` meta entries for the given order IDs
+	 * from the authoritative table and the backup table (when data sync is enabled).
+	 *
+	 * @param array $order_ids Array of order IDs to delete meta for.
+	 */
+	private function delete_debug_log_meta_entries( array $order_ids ): void {
+		global $wpdb;
+
+		$tables = array(
+			array(
+				'table'     => $this->hpos_in_use ? "{$wpdb->prefix}wc_orders_meta" : $wpdb->postmeta,
+				'id_column' => $this->hpos_in_use ? 'order_id' : 'post_id',
+			),
+		);
+
+		if ( $this->data_synchronizer->data_sync_is_enabled() ) {
+			$tables[] = array(
+				'table'     => $this->hpos_in_use ? $wpdb->postmeta : "{$wpdb->prefix}wc_orders_meta",
+				'id_column' => $this->hpos_in_use ? 'post_id' : 'order_id',
+			);
+		}
+
+		$id_placeholders = implode( ',', array_fill( 0, count( $order_ids ), '%d' ) );
+
+		foreach ( $tables as $table_config ) {
+			// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+			$wpdb->query(
+				$wpdb->prepare(
+					"DELETE FROM {$table_config['table']}
+					 WHERE {$table_config['id_column']} IN ({$id_placeholders})
+					 AND meta_key IN (%s, %s)",
+					array_merge( $order_ids, array( '_debug_log_source', '_debug_log_source_pending_deletion' ) )
+				)
+			);
+			// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
+		}
+	}
+}
diff --git a/plugins/woocommerce/src/Internal/Logging/OrderLogsDeletionProcessor.php b/plugins/woocommerce/src/Internal/Logging/OrderLogsDeletionProcessor.php
index 175634fd0ad..ba3957da5e6 100644
--- a/plugins/woocommerce/src/Internal/Logging/OrderLogsDeletionProcessor.php
+++ b/plugins/woocommerce/src/Internal/Logging/OrderLogsDeletionProcessor.php
@@ -6,7 +6,6 @@ namespace Automattic\WooCommerce\Internal\Logging;

 use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessorInterface;
 use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
-use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
 use Automattic\WooCommerce\Proxies\LegacyProxy;
 use Automattic\WooCommerce\Utilities\StringUtil;

@@ -44,11 +43,11 @@ class OrderLogsDeletionProcessor implements BatchProcessorInterface {
 	private LegacyProxy $legacy_proxy;

 	/**
-	 * The instance of DataSynchronizer to use.
+	 * The instance of OrderLogsCleanupHelper to use.
 	 *
-	 * @var DataSynchronizer
+	 * @var OrderLogsCleanupHelper
 	 */
-	private DataSynchronizer $data_synchronizer;
+	private OrderLogsCleanupHelper $order_logs_cleanup_helper;

 	/**
 	 * Initialize the instance.
@@ -56,18 +55,18 @@ class OrderLogsDeletionProcessor implements BatchProcessorInterface {
 	 *
 	 * @param CustomOrdersTableController $hpos_controller The instance of CustomOrdersTableController to use.
 	 * @param LegacyProxy                 $legacy_proxy The instance of LegacyProxy to use.
-	 * @param DataSynchronizer            $data_synchronizer The instance of DataSynchronizer to use.
+	 * @param OrderLogsCleanupHelper      $order_logs_cleanup_helper The instance of OrderLogsCleanupHelper to use.
 	 *
 	 * @internal
 	 */
-	final public function init( CustomOrdersTableController $hpos_controller, LegacyProxy $legacy_proxy, DataSynchronizer $data_synchronizer ) {
+	final public function init( CustomOrdersTableController $hpos_controller, LegacyProxy $legacy_proxy, OrderLogsCleanupHelper $order_logs_cleanup_helper ) {
 		$this->hpos_in_use = $hpos_controller->custom_orders_table_usage_is_enabled();
 		if ( ! $this->hpos_in_use ) {
 			$this->cpt_in_use = \WC_Order_Data_Store_CPT::class === \WC_Data_Store::load( 'order' )->get_current_class_name();
 		}

-		$this->legacy_proxy      = $legacy_proxy;
-		$this->data_synchronizer = $data_synchronizer;
+		$this->legacy_proxy              = $legacy_proxy;
+		$this->order_logs_cleanup_helper = $order_logs_cleanup_helper;
 	}

 	/**
@@ -235,52 +234,15 @@ class OrderLogsDeletionProcessor implements BatchProcessorInterface {
 			return;
 		}

-		$logger = $this->legacy_proxy->call_function( 'wc_get_logger' );
+		$items = array();
 		foreach ( $batch as $item ) {
 			if ( ! is_array( $item ) || ! isset( $item['meta_value'] ) || ! isset( $item['order_id'] ) ) {
 				throw new \Exception( "\$batch must be an array of arrays, each having a 'meta_value' key and an 'order_id' key" );
 			}
-			if ( $logger instanceof \WC_Logger ) {
-				$logger->clear( $item['meta_value'] );
-			}
-		}
-
-		$order_ids = array_map( 'absint', array_column( $batch, 'order_id' ) );
-
-		// Delete from the authoritative meta table.
-		$this->delete_debug_log_source_meta_entries( true, $order_ids );
-
-		if ( $this->data_synchronizer->data_sync_is_enabled() ) {
-			// When HPOS data sync is enabled we need to manually delete the entries in the backup meta table too,
-			// otherwise the next sync process will restore the rows we just deleted from the authoritative meta table.
-			$this->delete_debug_log_source_meta_entries( false, $order_ids );
+			$items[ $item['order_id'] ] = $item['meta_value'];
 		}
-	}

-	/**
-	 * Delete meta entries for the given order IDs.
-	 *
-	 * @param bool  $from_authoritative_table True to delete from the authoritative table, false for the backup table.
-	 * @param array $order_ids Array of order IDs to delete.
-	 */
-	private function delete_debug_log_source_meta_entries( bool $from_authoritative_table, array $order_ids ): void {
-		global $wpdb;
-
-		$use_hpos_table = $this->hpos_in_use === $from_authoritative_table;
-		$table_name     = $use_hpos_table ? "{$wpdb->prefix}wc_orders_meta" : $wpdb->postmeta;
-		$id_column_name = $use_hpos_table ? 'order_id' : 'post_id';
-		$placeholders   = implode( ',', array_fill( 0, count( $order_ids ), '%d' ) );
-
-		// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
-		$wpdb->query(
-			$wpdb->prepare(
-				"DELETE FROM {$table_name}
-				 WHERE {$id_column_name} IN ({$placeholders})
-				 AND meta_key = %s",
-				array_merge( $order_ids, array( '_debug_log_source_pending_deletion' ) )
-			)
-		);
-		// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+		$this->order_logs_cleanup_helper->clear_logs_and_delete_meta( $items );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Logging/OrderLogsDeletionProcessorTest.php b/plugins/woocommerce/tests/php/src/Internal/Logging/OrderLogsCleanupTest.php
similarity index 75%
rename from plugins/woocommerce/tests/php/src/Internal/Logging/OrderLogsDeletionProcessorTest.php
rename to plugins/woocommerce/tests/php/src/Internal/Logging/OrderLogsCleanupTest.php
index 213ee8c05d6..8ddc5755ca9 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Logging/OrderLogsDeletionProcessorTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Logging/OrderLogsCleanupTest.php
@@ -4,7 +4,11 @@ declare( strict_types=1 );

 namespace Automattic\WooCommerce\Tests\Internal\Logging;

+use Automattic\WooCommerce\Internal\Admin\Logging\FileV2\FileController;
+use Automattic\WooCommerce\Internal\Admin\Logging\LogHandlerFileV2;
+use Automattic\WooCommerce\Internal\Admin\Logging\Settings;
 use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
+use Automattic\WooCommerce\Internal\Logging\OrderLogsCleanupHelper;
 use Automattic\WooCommerce\Internal\Logging\OrderLogsDeletionProcessor;
 use Automattic\WooCommerce\Proxies\LegacyProxy;
 use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
@@ -12,9 +16,9 @@ use Automattic\WooCommerce\RestApi\UnitTests\HPOSToggleTrait;
 use Automattic\WooCommerce\Testing\Tools\TestingContainer;

 /**
- * Tests for the OrderLogsDeletionProcessor class.
+ * Tests for the OrderLogsDeletionProcessor and OrderLogsCleanupHelper classes.
  */
-class OrderLogsDeletionProcessorTest extends \WC_Unit_Test_Case {
+class OrderLogsCleanupTest extends \WC_Unit_Test_Case {
 	use HPOSToggleTrait;

 	/**
@@ -38,6 +42,13 @@ class OrderLogsDeletionProcessorTest extends \WC_Unit_Test_Case {
 	 */
 	private OrderLogsDeletionProcessor $sut;

+	/**
+	 * The cleanup helper instance.
+	 *
+	 * @var OrderLogsCleanupHelper
+	 */
+	private OrderLogsCleanupHelper $sut_cleanup_helper;
+
 	/**
 	 * The DI container to use.
 	 *
@@ -97,8 +108,13 @@ class OrderLogsDeletionProcessorTest extends \WC_Unit_Test_Case {
 		global $wpdb;
 		$wpdb->delete( $wpdb->prefix . 'wc_orders_meta', array( 'meta_key' => '_debug_log_source_pending_deletion' ) );
 		$wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_debug_log_source_pending_deletion' ) );
+		$wpdb->delete( $wpdb->prefix . 'wc_orders_meta', array( 'meta_key' => '_debug_log_source' ) );
+		$wpdb->delete( $wpdb->postmeta, array( 'meta_key' => '_debug_log_source' ) );

-		$this->sut = $this->container->get( OrderLogsDeletionProcessor::class );
+		self::delete_all_log_files();
+
+		$this->sut                = $this->container->get( OrderLogsDeletionProcessor::class );
+		$this->sut_cleanup_helper = $this->container->get( OrderLogsCleanupHelper::class );

 		$this->register_legacy_proxy_function_mocks(
 			array(
@@ -114,6 +130,7 @@ class OrderLogsDeletionProcessorTest extends \WC_Unit_Test_Case {
 	 * Runs after each test.
 	 */
 	public function tearDown(): void {
+		self::delete_all_log_files();
 		parent::tearDown();
 		if ( $this->data_store_filter_callback ) {
 				remove_filter( 'woocommerce_order_data_store', $this->data_store_filter_callback, 99999 );
@@ -441,6 +458,81 @@ class OrderLogsDeletionProcessorTest extends \WC_Unit_Test_Case {
 		return $order_ids;
 	}

+	/**
+	 * @testdox cleanup() deletes old place-order-debug log files whose modification date is at least 3 days in the past, but keeps recent ones.
+	 */
+	public function test_cleanup_deletes_old_log_files_but_keeps_recent_ones(): void {
+		$handler         = new LogHandlerFileV2();
+		$file_controller = wc_get_container()->get( FileController::class );
+
+		$handler->handle( time(), 'debug', 'old log entry', array( 'source' => 'place-order-debug-old' ) );
+		$handler->handle( time(), 'debug', 'recent log entry', array( 'source' => 'place-order-debug-recent' ) );
+
+		$old_files = $file_controller->get_files( array( 'source' => 'place-order-debug-old' ) );
+		$this->assertCount( 1, $old_files );
+
+		// Backdate the "old" file's modification time to 4 days ago.
+		touch( $old_files[0]->get_path(), time() - 4 * DAY_IN_SECONDS ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_touch
+
+		$recent_files = $file_controller->get_files( array( 'source' => 'place-order-debug-recent' ) );
+		$this->assertCount( 1, $recent_files );
+
+		$this->sut_cleanup_helper->cleanup();
+
+		$this->assertCount( 0, $file_controller->get_files( array( 'source' => 'place-order-debug-old' ) ) );
+		$this->assertCount( 1, $file_controller->get_files( array( 'source' => 'place-order-debug-recent' ) ) );
+	}
+
+	/**
+	 * @testdox cleanup() does not touch orders that have _debug_log_source_pending_deletion meta (handled by the batch processor) even if older than 3 days.
+	 *
+	 * @testWith [true]
+	 *           [false]
+	 *
+	 * @param bool $with_hpos Test with HPOS active or not.
+	 */
+	public function test_cleanup_does_not_touch_orders_with_pending_deletion_meta( bool $with_hpos ): void {
+		$this->setup_hpos_and_reset_container( $with_hpos );
+
+		$order = OrderHelper::create_order();
+		$order->set_date_created( strtotime( '-5 days' ) );
+		$order->add_meta_data( '_debug_log_source_pending_deletion', 'place-order-debug-batch', true );
+		$order->save();
+
+		$this->sut_cleanup_helper->cleanup();
+
+		$this->assertEquals( 1, $this->sut->get_total_pending_count() );
+	}
+
+	/**
+	 * @testdox cleanup() deletes _debug_log_source meta only from orders older than 3 days and leaves recent ones untouched.
+	 *
+	 * @testWith [true]
+	 *           [false]
+	 *
+	 * @param bool $with_hpos Test with HPOS active or not.
+	 */
+	public function test_cleanup_deletes_stale_debug_log_source_meta_only_when_old( bool $with_hpos ): void {
+		$this->setup_hpos_and_reset_container( $with_hpos );
+
+		$old_order = OrderHelper::create_order();
+		$old_order->set_date_created( strtotime( '-5 days' ) );
+		$old_order->add_meta_data( '_debug_log_source', 'place-order-debug-stale', true );
+		$old_order->save();
+
+		$recent_order = OrderHelper::create_order();
+		$recent_order->add_meta_data( '_debug_log_source', 'place-order-debug-fresh', true );
+		$recent_order->save();
+
+		$this->sut_cleanup_helper->cleanup();
+
+		$old_order_reloaded = wc_get_order( $old_order->get_id() );
+		$this->assertEmpty( $old_order_reloaded->get_meta( '_debug_log_source' ) );
+
+		$recent_order_reloaded = wc_get_order( $recent_order->get_id() );
+		$this->assertEquals( 'place-order-debug-fresh', $recent_order_reloaded->get_meta( '_debug_log_source' ) );
+	}
+
 	/**
 	 * Initialize HPOS and reset the DI container resolutions
 	 * (resetting the container is needed because the tested class checks for HPOS activation
@@ -451,6 +543,24 @@ class OrderLogsDeletionProcessorTest extends \WC_Unit_Test_Case {
 	private function setup_hpos_and_reset_container( bool $enable_hpos ) {
 		$this->toggle_cot_feature_and_usage( $enable_hpos );
 		$this->container->reset_all_resolved();
-		$this->sut = $this->container->get( OrderLogsDeletionProcessor::class );
+		$this->sut                = $this->container->get( OrderLogsDeletionProcessor::class );
+		$this->sut_cleanup_helper = $this->container->get( OrderLogsCleanupHelper::class );
+	}
+
+	/**
+	 * Delete all place-order-debug log files using the FileV2 controller.
+	 */
+	private static function delete_all_log_files(): void {
+		$file_controller = wc_get_container()->get( FileController::class );
+		$files           = $file_controller->get_files(
+			array(
+				'source'   => 'place-order-debug',
+				'per_page' => 1000,
+			)
+		);
+
+		if ( is_array( $files ) && ! empty( $files ) ) {
+			$file_controller->delete_files( array_map( fn( $file ) => $file->get_file_id(), $files ) );
+		}
 	}
 }